HTTP клиент на TypeScript: делаем запросы правильно
В современной веб-разработке работа с HTTP-запросами является одной из ключевых задач. TypeScript, как строго типизированная надстройка над JavaScript, предоставляет разработчикам мощные инструменты для создания надежных и предсказуемых HTTP-клиентов. В этой статье мы разберем, как правильно организовать HTTP-клиент на TypeScript, чтобы ваш код был безопасным, читаемым и легко поддерживаемым.
Почему TypeScript идеален для HTTP-клиентов?
TypeScript добавляет статическую типизацию, что особенно важно при работе с внешними API. Вы точно знаете, какие данные придут от сервера, и можете предотвратить множество ошибок на этапе компиляции. Рассмотрим базовый пример:
// Плохо: JavaScript без типов
const data = await fetch('/api/users');
const users = await data.json();
// Мы не знаем структуру users
// Хорошо: TypeScript с типами
interface User {
id: number;
name: string;
email: string;
}
const response = await fetch('/api/users');
const users: User[] = await response.json();
// Теперь users имеет четкую структуру
Создание базового HTTP-клиента на TypeScript
Начнем с создания универсального HTTP-клиента, который будет обрабатывать все типы запросов. Используем нативный fetch API, который доступен в современных браузерах и Node.js 18+.
Базовая реализация с типизацией
interface HttpClientConfig {
baseURL: string;
headers?: Record<string, string>;
timeout?: number;
}
interface ApiResponse<T> {
data: T;
status: number;
ok: boolean;
}
class HttpClient {
private baseURL: string;
private headers: Record<string, string>;
private timeout: number;
constructor(config: HttpClientConfig) {
this.baseURL = config.baseURL;
this.headers = {
'Content-Type': 'application/json',
...config.headers,
};
this.timeout = config.timeout || 10000;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseURL}${endpoint}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
...options,
headers: { ...this.headers, ...options.headers },
signal: controller.signal,
});
clearTimeout(timeoutId);
const data: T = await response.json();
return {
data,
status: response.status,
ok: response.ok,
};
} catch (error) {
clearTimeout(timeoutId);
throw this.handleError(error);
}
}
private handleError(error: unknown): Error {
if (error instanceof DOMException && error.name === 'AbortError') {
return new Error('Request timeout');
}
if (error instanceof TypeError) {
return new Error('Network error');
}
return error as Error;
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'GET' });
}
async post<T>(endpoint: string, body: unknown): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(body),
});
}
async put<T>(endpoint: string, body: unknown): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(body),
});
}
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
Использование клиента
// Создаем экземпляр клиента
const api = new HttpClient({
baseURL: 'https://api.example.com',
timeout: 5000,
});
// Определяем типы данных
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
// GET запрос
async function getPosts() {
try {
const response = await api.get<Post[]>('/posts');
console.log(response.data); // Post[]
} catch (error) {
console.error('Failed to fetch posts:', error);
}
}
// POST запрос
async function createPost(post: Omit<Post, 'id'>) {
try {
const response = await api.post<Post>('/posts', post);
return response.data;
} catch (error) {
console.error('Failed to create post:', error);
throw error;
}
}
Продвинутые паттерны для HTTP-клиента
Базовая реализация — это только начало. Рассмотрим более сложные сценарии, которые часто встречаются в реальных проектах.
Интерцепторы (перехватчики)
Интерцепторы позволяют обрабатывать запросы и ответы глобально. Это полезно для добавления токенов аутентификации, логирования или обработки ошибок.
interface Interceptor {
onRequest?: (config: RequestInit) => RequestInit;
onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T>;
onError?: (error: Error) => void;
}
class HttpClientWithInterceptors extends HttpClient {
private interceptors: Interceptor[] = [];
addInterceptor(interceptor: Interceptor): void {
this.interceptors.push(interceptor);
}
private applyRequestInterceptors(options: RequestInit): RequestInit {
return this.interceptors.reduce(
(config, interceptor) =>
interceptor.onRequest ? interceptor.onRequest(config) : config,
options
);
}
private applyResponseInterceptors<T>(response: ApiResponse<T>): ApiResponse<T> {
return this.interceptors.reduce(
(resp, interceptor) =>
interceptor.onResponse ? interceptor.onResponse(resp) : resp,
response
);
}
private applyErrorInterceptors(error: Error): void {
this.interceptors.forEach(interceptor => {
interceptor.onError?.(error);
});
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
try {
const options = this.applyRequestInterceptors({ method: 'GET' });
const response = await this.request<T>(endpoint, options);
return this.applyResponseInterceptors(response);
} catch (error) {
this.applyErrorInterceptors(error as Error);
throw error;
}
}
}
// Пример использования
const apiWithInterceptors = new HttpClientWithInterceptors({
baseURL: 'https://api.example.com',
});
// Добавляем интерцептор для авторизации
apiWithInterceptors.addInterceptor({
onRequest: (config) => ({
...config,
headers: {
...config.headers,
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
}),
onError: (error) => {
if (error.message.includes('401')) {
// Перенаправляем на страницу логина
window.location.href = '/login';
}
},
});