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

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

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

Начать курс

Продвинутый HTTP клиент на TypeScript: от базовых запросов к промышленным решениям

При разработке современных веб-приложений на TypeScript работа с HTTP-запросами выходит далеко за рамки простого вызова fetch. На продвинутом уровне требуется не только отправлять запросы, но и грамотно обрабатывать ошибки, управлять временем ожидания, типизировать ответы, внедрять интерсепторы и даже кэшировать данные. В этой статье мы разберём, как построить надёжный и гибкий HTTP-клиент на TypeScript, который подойдёт для production-проектов.

1. Создание типизированного HTTP-клиента с использованием обобщений (Generics)

Одно из главных преимуществ TypeScript — строгая типизация. Создадим базовый класс HttpClient, который будет принимать тип ответа через дженерики. Это позволит избежать ошибок на этапе компиляции.

// types.ts
export interface ApiResponse<T> {
  data: T;
  status: number;
  ok: boolean;
}

export interface RequestConfig {
  headers?: Record<string, string>;
  params?: Record<string, string>;
  signal?: AbortSignal;
}

// http-client.ts
import { ApiResponse, RequestConfig } from './types';

class HttpClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>> {
    const url = new URL(`${this.baseUrl}${endpoint}`);
    if (config?.params) {
      Object.entries(config.params).forEach(([key, value]) => {
        url.searchParams.append(key, value);
      });
    }

    const response = await fetch(url.toString(), {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...config?.headers,
      },
      signal: config?.signal,
    });

    const data: T = await response.json();
    return {
      data,
      status: response.status,
      ok: response.ok,
    };
  }

  async post<T, U>(endpoint: string, body: U, config?: RequestConfig): Promise<ApiResponse<T>> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...config?.headers,
      },
      body: JSON.stringify(body),
      signal: config?.signal,
    });

    const data: T = await response.json();
    return { data, status: response.status, ok: response.ok };
  }
}

export default HttpClient;

2. Обработка ошибок и таймаутов

Продвинутый клиент должен корректно обрабатывать как сетевые ошибки, так и ошибки бизнес-логики. Добавим механизм таймаута с помощью AbortController и кастомную обработку ошибок.

// errors.ts
export class HttpError extends Error {
  status: number;
  data?: unknown;

  constructor(message: string, status: number, data?: unknown) {
    super(message);
    this.name = 'HttpError';
    this.status = status;
    this.data = data;
  }
}

export class TimeoutError extends Error {
  constructor(timeout: number) {
    super(`Request timed out after ${timeout}ms`);
    this.name = 'TimeoutError';
  }
}

// http-client.ts (дополнение)
async requestWithTimeout<T>(
  endpoint: string,
  options: RequestInit,
  timeout: number = 5000
): Promise<ApiResponse<T>> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      signal: controller.signal,
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => null);
      throw new HttpError(
        `HTTP ${response.status}: ${response.statusText}`,
        response.status,
        errorData
      );
    }

    const data: T = await response.json();
    return { data, status: response.status, ok: true };
  } catch (error) {
    if (error instanceof DOMException && error.name === 'AbortError') {
      throw new TimeoutError(timeout);
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

3. Интерсепторы: перехват запросов и ответов

Интерсепторы — мощный паттерн, позволяющий модифицировать запросы (добавлять токены, логирование) или обрабатывать ответы централизованно. Реализуем очередь интерсепторов.

// interceptors.ts
export interface Interceptor<T> {
  onFulfilled?: (value: T) => T | Promise<T>;
  onRejected?: (error: unknown) => unknown;
}

class InterceptorManager<T> {
  private interceptors: Interceptor<T>[] = [];

  use(onFulfilled?: (value: T) => T | Promise<T>, onRejected?: (error: unknown) => unknown): number {
    this.interceptors.push({ onFulfilled, onRejected });
    return this.interceptors.length - 1;
  }

  eject(id: number): void {
    if (this.interceptors[id]) {
      this.interceptors[id] = { onFulfilled: undefined, onRejected: undefined };
    }
  }

  async execute(initialValue: T): Promise<T> {
    let value = initialValue;
    for (const interceptor of this.interceptors) {
      if (interceptor.onFulfilled) {
        value = await interceptor.onFulfilled(value);
      }
    }
    return value;
  }
}

// Использование в HttpClient
class HttpClient {
  public requestInterceptors = new InterceptorManager<RequestInit>();
  public responseInterceptors = new InterceptorManager<ApiResponse<unknown>>();

  async get<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>> {
    let options: RequestInit = {
      method: 'GET',
      headers: { 'Content-Type': 'application/json', ...config?.headers },
      signal: config?.signal,
    };

    // Применяем интерсепторы запроса
    options = await this.requestInterceptors.execute(options);

    let response = await fetch(`${this.baseUrl}${endpoint}`, options);
    let data: T = await response.json();
    let apiResponse: ApiResponse<T> = { data, status: response.status, ok: response.ok };

    // Применяем интерсепторы ответа
    apiResponse = await this.responseInterceptors.execute(apiResponse) as ApiResponse<T>;
    return apiResponse;
  }
}

Пример использования интерсептора для добавления токена авторизации:

const client = new HttpClient('https://api.example.com');

client.requestInterceptors.use(async (config) => {
  const token = await getAuthToken();
  return {
    ...config,
    headers: {
      ...config.headers,
      'Authorization': `Bearer ${token}`,
    },
  };
});

4. Кэширование запросов для оптимизации производительности

Блоги

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