dramaling-vocab-learning/frontend/lib/api/client.ts

212 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 統一的API客戶端
* 提供一致的請求處理、錯誤處理和認證邏輯
*/
import {
createApiErrorFromResponse,
createApiErrorFromException,
createSuccessResponse,
createErrorResponse,
StandardApiResponse,
type ApiError
} from './errorHandler'
export interface RequestConfig extends RequestInit {
timeout?: number
retries?: number
context?: string
}
export class ApiClient {
private baseURL: string
private defaultTimeout: number = 10000 // 10秒
private defaultRetries: number = 3
constructor(baseURL?: string) {
this.baseURL = baseURL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'
}
/**
* 統一的HTTP請求方法
*/
async request<T>(
endpoint: string,
config: RequestConfig = {}
): Promise<StandardApiResponse<T>> {
const {
timeout = this.defaultTimeout,
retries = this.defaultRetries,
context,
...fetchOptions
} = config
const url = `${this.baseURL}${endpoint}`
let lastError: ApiError
// 重試邏輯
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...this.getDefaultHeaders(),
...fetchOptions.headers,
},
})
clearTimeout(timeoutId)
if (!response.ok) {
const apiError = await createApiErrorFromResponse(response, context)
lastError = apiError
// 如果不可重試,直接返回錯誤
if (attempt === retries || !this.isRetryableStatus(response.status)) {
return createErrorResponse(apiError)
}
// 等待後重試
await this.delay(Math.pow(2, attempt) * 1000) // 指數退避
continue
}
// 成功情況
const data = await response.json()
return createSuccessResponse(data)
} catch (error) {
const apiError = createApiErrorFromException(error, context)
lastError = apiError
// 超時或網路錯誤才重試
if (attempt === retries || !this.isRetryableError(apiError)) {
return createErrorResponse(apiError)
}
await this.delay(Math.pow(2, attempt) * 1000)
}
}
return createErrorResponse(lastError!)
}
/**
* GET請求
*/
async get<T>(endpoint: string, config?: RequestConfig): Promise<StandardApiResponse<T>> {
return this.request<T>(endpoint, { ...config, method: 'GET' })
}
/**
* POST請求
*/
async post<T>(endpoint: string, data?: any, config?: RequestConfig): Promise<StandardApiResponse<T>> {
return this.request<T>(endpoint, {
...config,
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
})
}
/**
* PUT請求
*/
async put<T>(endpoint: string, data?: any, config?: RequestConfig): Promise<StandardApiResponse<T>> {
return this.request<T>(endpoint, {
...config,
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
})
}
/**
* DELETE請求
*/
async delete<T>(endpoint: string, config?: RequestConfig): Promise<StandardApiResponse<T>> {
return this.request<T>(endpoint, { ...config, method: 'DELETE' })
}
/**
* 獲取預設Headers
*/
private getDefaultHeaders(): Record<string, string> {
const headers: Record<string, string> = {}
// 開發階段不添加認證Token
// TODO: 生產環境需要添加認證邏輯
// const token = localStorage.getItem('auth_token')
// if (token) {
// headers.Authorization = `Bearer ${token}`
// }
return headers
}
/**
* 判斷HTTP狀態碼是否可重試
*/
private isRetryableStatus(status: number): boolean {
return [
408, // Request Timeout
429, // Too Many Requests
500, // Internal Server Error
502, // Bad Gateway
503, // Service Unavailable
504, // Gateway Timeout
].includes(status)
}
/**
* 判斷錯誤是否可重試
*/
private isRetryableError(error: ApiError): boolean {
return [
'NETWORK_ERROR',
'TIMEOUT_ERROR',
'SERVER_ERROR'
].includes(error.type)
}
/**
* 延遲函數
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* 設置基礎URL
*/
setBaseURL(url: string): void {
this.baseURL = url
}
/**
* 設置預設超時時間
*/
setTimeout(timeout: number): void {
this.defaultTimeout = timeout
}
/**
* 設置預設重試次數
*/
setRetries(retries: number): void {
this.defaultRetries = retries
}
}
// 建立預設的API客戶端實例
export const apiClient = new ApiClient()
// 建立特定服務的API客戶端
export const authApiClient = new ApiClient(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api/auth`)
export const flashcardsApiClient = new ApiClient(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`)
export const studyApiClient = new ApiClient(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`)
export const imageApiClient = new ApiClient(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`)