本文 AI 產出,尚未審核

Axios 呼叫型別設計

簡介

在前端專案中,HTTP 請求是與後端服務溝通的唯一通道。隨著專案規模的擴大,單純使用 axios.get(...).then(...) 的寫法很快會變得雜亂,尤其在 TypeScript 環境下,若沒有適當的型別設計,IDE 的自動完成、靜態檢查與重構支援都會大幅退化。

本篇文章聚焦於 「Axios 呼叫型別設計」,說明如何在 TypeScript 中建立可重用、可維護且安全的 HTTP 客戶端。從基本的請求回傳型別、錯誤處理,到進階的泛型封裝與拦截器型別,提供完整的實務範例,協助初學者與中階開發者快速上手,並在大型專案中落實最佳實踐。


核心概念

1. 為何要為 Axios 定義型別?

  • 靜態檢查:避免因錯誤的屬性名稱或資料結構導致執行時例外。
  • 自動完成:IDE 能正確提示回傳物件的欄位,提升開發效率。
  • 文件生成:型別即是最好的文件,團隊成員只要閱讀型別即可了解 API 合約。

2. 基本的 Response 型別

// api/types.ts
export interface ApiResponse<T = any> {
  /** 後端回傳的狀態碼,例如 200、404 */
  status: number;
  /** 後端自訂的業務代碼,0 代表成功 */
  code: number;
  /** 回傳的資料,使用泛型 T 讓呼叫端自行決定型別 */
  data: T;
  /** 錯誤訊息,成功時為 null */
  message: string | null;
}

說明ApiResponse<T> 把後端慣用的 status / code / data / message 結構抽象化。使用泛型 T,呼叫端只要傳入正確的資料型別,即可在編譯階段得到完整的型別提示。

3. 建立通用的 Axios Instance

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { ApiResponse } from './types';

const baseConfig: AxiosRequestConfig = {
  baseURL: import.meta.env.VITE_API_BASE, // Vite 環境變數
  timeout: 10_000,
  headers: { 'Content-Type': 'application/json' },
};

const client: AxiosInstance = axios.create(baseConfig);

/**
 * 讓所有回傳自動套用 ApiResponse<T> 型別
 */
client.interceptors.response.use(
  (res: AxiosResponse<ApiResponse>) => res,
  (err) => Promise.reject(err)
);

export default client;

重點:在拦截器裡把 AxiosResponse 的泛型指定為 ApiResponse,這樣之後的 client.get<...>() 會自動得到正確的型別。

4. 使用泛型封裝 CRUD 方法

// api/request.ts
import client from './client';
import type { ApiResponse } from './types';
import type { AxiosRequestConfig } from 'axios';

/**
 * 取得資料 (GET)
 */
export async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
  const response = await client.get<ApiResponse<T>>(url, config);
  if (response.data.code !== 0) {
    throw new Error(response.data.message ?? '未知錯誤');
  }
  return response.data.data;
}

/**
 * 新增資料 (POST)
 */
export async function post<T, R = any>(url: string, payload: T, config?: AxiosRequestConfig): Promise<R> {
  const response = await client.post<ApiResponse<R>>(url, payload, config);
  if (response.data.code !== 0) {
    throw new Error(response.data.message ?? '未知錯誤');
  }
  return response.data.data;
}

/**
 * 更新資料 (PUT)
 */
export async function put<T, R = any>(url: string, payload: T, config?: AxiosRequestConfig): Promise<R> {
  const response = await client.put<ApiResponse<R>>(url, payload, config);
  if (response.data.code !== 0) {
    throw new Error(response.data.message ?? '未知錯誤');
  }
  return response.data.data;
}

/**
 * 刪除資料 (DELETE)
 */
export async function del<R = any>(url: string, config?: AxiosRequestConfig): Promise<R> {
  const response = await client.delete<ApiResponse<R>>(url, config);
  if (response.data.code !== 0) {
    throw new Error(response.data.message ?? '未知錯誤');
  }
  return response.data.data;
}

說明

  • get<T>post<T, R> 等函式皆採用 雙泛型(請求資料與回傳資料),讓呼叫端可以同時限定送出的 payload 以及期待的回傳型別。
  • 若後端回傳的 code 不是 0,就拋出錯誤,統一錯誤處理機制。

5. 範例:取得使用者列表

// models/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'member';
}

// services/userService.ts
import { get } from '@/api/request';
import type { User } from '@/models/user';

export async function fetchUserList(): Promise<User[]> {
  // 這裡的 <User[]> 會自動推斷回傳型別為 User[]
  return get<User[]>('/users');
}

使用方式:

// component/UserList.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { fetchUserList } from '@/services/userService';
import type { User } from '@/models/user';

const users = ref<User[]>([]);
const loading = ref(true);
const error = ref<string | null>(null);

onMounted(async () => {
  try {
    users.value = await fetchUserList();
  } catch (e) {
    error.value = (e as Error).message;
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <div v-if="loading">載入中…</div>
  <div v-else-if="error">錯誤:{{ error }}</div>
  <ul v-else>
    <li v-for="u in users" :key="u.id">{{ u.name }} ({{ u.role }})</li>
  </ul>
</template>

重點users 的型別在整個組件中都保持正確,IDE 能即時提示 idnameemail 等屬性,減少手寫錯字的機會。

6. 進階:為拦截器加上自訂錯誤型別

// api/error.ts
export interface ApiError extends Error {
  /** 後端回傳的錯誤代碼 */
  code: number;
  /** HTTP 狀態碼 */
  status: number;
  /** 原始回傳資料 */
  payload?: any;
}

/**
 * 把 AxiosError 轉成統一的 ApiError
 */
export function toApiError(err: any): ApiError {
  const apiError: ApiError = new Error(err.message);
  apiError.code = err.response?.data?.code ?? -1;
  apiError.status = err.response?.status ?? 0;
  apiError.payload = err.response?.data ?? null;
  return apiError;
}

client.ts 中加入:

import { toApiError } from './error';

client.interceptors.response.use(
  (res) => res,
  (err) => Promise.reject(toApiError(err))
);

好處:全局拦截器將所有錯誤統一為 ApiError,呼叫端只要捕捉一次即可取得 codestatus 與原始 payload,方便根據業務需求顯示不同的錯誤訊息。


常見陷阱與最佳實踐

陷阱 說明 解決方式
未使用泛型 直接寫 axios.get('/users'),回傳型別為 any,失去 TypeScript 的保護。 為每一次請求指定 <ApiResponse<T>>,或使用封裝好的 get<T>()
拦截器返回 any 拦截器的回傳若未指定型別,後續的 response.data 仍會是 any 在拦截器中明確寫 AxiosResponse<ApiResponse>,或在 client.ts 中使用 as const
錯誤拋出不是 Error 有時直接 throw response.data,導致 catch 取得的不是 Error 物件。 統一使用 toApiError 產生 ApiError,並在 catch 時檢查 instanceof Error
URL 拼接錯誤 手寫字串拼接容易遺漏斜線或參數編碼。 使用 URLSearchParamsaxiosparams 參數,保持 URL 與 query 分離。
重複設定 headers 每次呼叫都在 config 裡寫 headers,易產生不一致。 client.ts 中統一設定預設 header,僅在需要時覆寫。

最佳實踐

  1. 集中管理 API 型別:所有介面(如 User, Post)放在 models/types/ 資料夾,避免散落。
  2. 統一錯誤處理:利用拦截器與 ApiError,在 UI 層只需要捕捉一次即可。
  3. 使用環境變數baseURLtimeout 等設定放在 .env,不同環境只要改變變數即可。
  4. 測試型別:使用 tsdtype-tests 來驗證自訂的泛型函式在編譯時的正確性。
  5. 避免過度抽象:若某個 API 回傳結構與其他不同,別硬塞進 ApiResponse<T>,可以另建專屬型別。

實際應用場景

1. 多租戶 SaaS 平台

在 SaaS 系統中,每個租戶都有自己的 API 金鑰與不同的基礎路徑。透過 工廠函式 產生帶有租戶資訊的 Axios Instance,並結合前述的型別封裝,即可在同一專案中安全地同時呼叫多個租戶的服務。

// api/multiTenantClient.ts
import axios from 'axios';
import type { ApiResponse } from './types';
import { toApiError } from './error';

export function createTenantClient(tenantId: string, token: string) {
  const instance = axios.create({
    baseURL: `${import.meta.env.VITE_API_BASE}/${tenantId}`,
    headers: { Authorization: `Bearer ${token}` },
  });

  instance.interceptors.response.use(
    (res) => res,
    (err) => Promise.reject(toApiError(err))
  );

  return {
    get: <T>(url: string, cfg?: any) => instance.get<ApiResponse<T>>(url, cfg).then(r => r.data.data),
    post: <T, R>(url: string, payload: T, cfg?: any) => instance.post<ApiResponse<R>>(url, payload, cfg).then(r => r.data.data),
    // ...其他方法
  };
}

好處:每個租戶的請求都有自己的型別、錯誤處理與認證資訊,且不會相互污染。

2. 前端微服務聚合

在微前端或 BFF(Backend For Frontend)架構下,前端會同時呼叫多個後端微服務。利用 泛型合併,可以一次性取得多個服務的資料,且每個子資料都有明確型別。

interface DashboardData {
  user: User;
  stats: {
    posts: number;
    comments: number;
  };
  notifications: Notification[];
}

// 合併多個請求
export async function fetchDashboard(): Promise<DashboardData> {
  const [user, stats, notifications] = await Promise.all([
    get<User>('/users/me'),
    get<{ posts: number; comments: number }>('/stats'),
    get<Notification[]>('/notifications')
  ]);

  return { user, stats, notifications };
}

3. 跨平台 React Native + Web 共用 API 層

將上述的 request.ts 放在一個 純 TypeScript 的資料夾,React Native 與 Web 都可以直接 import 使用,無需針對平台寫兩套 HTTP 客戶端。只要在 client.ts 裡把 baseURL 改成環境變數即可。


總結

  • 型別化的 Axios 呼叫 能讓開發者在編譯階段即捕捉錯誤、提升 IDE 補全與文件可讀性。
  • 透過 泛型封裝get<T>()post<T, R>())與 統一的 ApiResponse<T>,每一次 API 呼叫都能明確知道「送什麼」與「收什麼」。
  • 拦截器 + 自訂 ApiError 為全局錯誤處理提供一致的介面,避免散落在各個 catch 裡的雜訊程式碼。
  • 多租戶、微服務聚合、跨平台 等實務場景中,只要把型別與客戶端抽象化,就能快速擴展、降低維護成本。

掌握了上述的型別設計技巧後,你的 TypeScript 專案不僅會變得更安全、更易於維護,也能在團隊協作時減少溝通成本。祝你在實務開發中玩得開心,寫出高品質的 Axios 呼叫程式碼!