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 能即時提示id、name、
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,呼叫端只要捕捉一次即可取得code、status與原始 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 拼接錯誤 | 手寫字串拼接容易遺漏斜線或參數編碼。 | 使用 URLSearchParams 或 axios 的 params 參數,保持 URL 與 query 分離。 |
| 重複設定 headers | 每次呼叫都在 config 裡寫 headers,易產生不一致。 |
在 client.ts 中統一設定預設 header,僅在需要時覆寫。 |
最佳實踐:
- 集中管理 API 型別:所有介面(如
User,Post)放在models/或types/資料夾,避免散落。 - 統一錯誤處理:利用拦截器與
ApiError,在 UI 層只需要捕捉一次即可。 - 使用環境變數:
baseURL、timeout等設定放在.env,不同環境只要改變變數即可。 - 測試型別:使用
tsd或type-tests來驗證自訂的泛型函式在編譯時的正確性。 - 避免過度抽象:若某個 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 呼叫程式碼!