API-клиент с TypeScript
Типизированная обёртка над
fetchс автоматической обработкой ошибок, базовым URL и JSON-парсингом.
Задача
В каждом проекте нужен единый способ обращаться к REST API: задать baseURL, добавить заголовки авторизации, обработать HTTP-ошибки и вернуть типизированный результат. Копировать fetch в каждом модуле — плохая практика.
Решение
// api-client.ts
interface RequestOptions extends RequestInit {
params?: Record<string, string | number>;
}
class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
message: string
) {
super(message);
this.name = 'ApiError';
}
}
class ApiClient {
private baseURL: string;
private defaultHeaders: HeadersInit;
constructor(baseURL: string, headers: HeadersInit = {}) {
this.baseURL = baseURL.replace(/\/$/, '');
this.defaultHeaders = {
'Content-Type': 'application/json',
...headers,
};
}
private buildURL(path: string, params?: Record<string, string | number>): string {
const url = new URL(`${this.baseURL}${path}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
}
return url.toString();
}
async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { params, ...init } = options;
const url = this.buildURL(path, params);
const response = await fetch(url, {
...init,
headers: {
...this.defaultHeaders,
...init.headers,
},
});
if (!response.ok) {
throw new ApiError(response.status, response.statusText, `HTTP ${response.status}: ${response.statusText}`);
}
// 204 No Content — пустой ответ
if (response.status === 204) return undefined as T;
return response.json() as Promise<T>;
}
get<T>(path: string, options?: RequestOptions) {
return this.request<T>(path, { ...options, method: 'GET' });
}
post<T>(path: string, body: unknown, options?: RequestOptions) {
return this.request<T>(path, {
...options,
method: 'POST',
body: JSON.stringify(body),
});
}
put<T>(path: string, body: unknown, options?: RequestOptions) {
return this.request<T>(path, {
...options,
method: 'PUT',
body: JSON.stringify(body),
});
}
patch<T>(path: string, body: unknown, options?: RequestOptions) {
return this.request<T>(path, {
...options,
method: 'PATCH',
body: JSON.stringify(body),
});
}
delete<T>(path: string, options?: RequestOptions) {
return this.request<T>(path, { ...options, method: 'DELETE' });
}
}
// Инстанс для проекта
export const api = new ApiClient('https://api.example.com', {
Authorization: `Bearer ${localStorage.getItem('token') ?? ''}`,
});
Использование:
interface User {
id: number;
name: string;
email: string;
}
// GET /users?page=1
const users = await api.get<User>('/users', { params: { page: 1 } });
// POST /users
const newUser = await api.post<User>('/users', { name: 'Alice', email: 'alice@example.com' });
// Обработка ошибок
try {
const user = await api.get<User>('/users/999');
} catch (err) {
if (err instanceof ApiError && err.status === 404) {
console.log('Пользователь не найден');
}
}
Ключевые моменты
extends RequestInit— не ломает совместимость с нативнымfetch, все опции (signal, credentials и т.д.) работают.- Дженерик
<T>даёт автодополнение в IDE и проверку типов на возвращаемый объект. ApiErrorнаследуетError— можно ловить черезinstanceofи отличать сетевые ошибки от HTTP.buildURLчерезURLбезопасно кодирует параметры; не конкатенируй строки вручную.
Варианты
- axios — популярная библиотека с перехватчиками (interceptors), отменой и широкой экосистемой.
- ky — крошечная fetch-обёртка с retry и хуками, поддерживает TypeScript из коробки.
- openapi-typescript — генерирует типы из OpenAPI-схемы, убирает ручное написание интерфейсов.