HTTP клиент на TypeScript: делаем запросы правильно

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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';
    }
  },
});

Блоги

Рекомендаций по книгам