API Integration¶
API Integration¶
Axios Client Configuration¶
Centralized Axios instance with interceptors:
// src/lib/api-client.ts
import axios from 'axios';
import { getSession } from '@/lib/auth';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Auth interceptor
apiClient.interceptors.request.use(async (config) => {
const session = await getSession();
if (session?.accessToken) {
config.headers.Authorization = `Bearer ${session.accessToken}`;
}
return config;
});
// Error interceptor
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Handle token refresh or redirect to login
await handleUnauthorized();
}
return Promise.reject(parseApiError(error));
}
);
Error Parsing¶
Consistent error handling:
// src/lib/errors.ts
export interface ApiError {
message: string;
code: string;
status: number;
details?: Record<string, string[]>;
}
export function parseApiError(error: unknown): ApiError {
if (axios.isAxiosError(error) && error.response) {
const { data, status } = error.response;
return {
message: data.message ?? 'An error occurred',
code: data.code ?? 'UNKNOWN_ERROR',
status,
details: data.details,
};
}
return {
message: 'Network error',
code: 'NETWORK_ERROR',
status: 0,
};
}
Hook Organization¶
One file per resource:
// src/hooks/api/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
import type { User, UserCreate, UserUpdate } from '@/types';
const KEYS = {
all: ['users'] as const,
list: (params?: UserListParams) => [...KEYS.all, 'list', params] as const,
detail: (id: string) => [...KEYS.all, 'detail', id] as const,
};
export function useUsers(params?: UserListParams) {
return useQuery({
queryKey: KEYS.list(params),
queryFn: () => apiClient.get<User[]>('/users', { params }).then(r => r.data),
});
}
export function useUser(id: string) {
return useQuery({
queryKey: KEYS.detail(id),
queryFn: () => apiClient.get<User>(`/users/${id}`).then(r => r.data),
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UserCreate) =>
apiClient.post<User>('/users', data).then(r => r.data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.all });
},
});
}
export function useUpdateUser(id: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UserUpdate) =>
apiClient.patch<User>(`/users/${id}`, data).then(r => r.data),
onSuccess: (user) => {
queryClient.setQueryData(KEYS.detail(id), user);
queryClient.invalidateQueries({ queryKey: KEYS.list() });
},
});
}
See Also¶
- Error Contracts -- Backend error response format consumed by the API client
- Backend API Design -- Server-side conventions for endpoints, methods, and responses