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

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

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

Начать курс

Введение: почему правильный HTTP клиент — основа современного веб-приложения

Любое современное веб-приложение взаимодействует с сервером через HTTP. Будь то загрузка списка товаров, отправка формы регистрации или получение данных для дашборда — всё это сводится к отправке HTTP-запросов. На TypeScript, с его мощной системой типов, можно построить не просто рабочий, а по-настоящему надёжный и предсказуемый HTTP-клиент.

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

Основы: от сырого fetch к типизированному клиенту

Начнём с базового примера. Встроенный fetch в JavaScript — хорошая отправная точка, но он не предоставляет удобной типизации и обработки ошибок. На TypeScript мы можем обернуть его в собственную функцию.

Простая обёртка для fetch

Создадим базовый класс HttpClient, который будет принимать базовый URL и настройки.

interface RequestOptions {
  headers?: Record<string, string>;
  params?: Record<string, string>;
}

class HttpClient {
  private baseUrl: string;
  private defaultHeaders: Record<string, string>;

  constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
    this.baseUrl = baseUrl;
    this.defaultHeaders = defaultHeaders;
  }

  private buildUrl(path: string, params?: Record<string, string>): string {
    const url = new URL(`${this.baseUrl}${path}`);
    if (params) {
      Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value));
    }
    return url.toString();
  }

  async get<T>(path: string, options?: RequestOptions): Promise<T> {
    const url = this.buildUrl(path, options?.params);
    const response = await fetch(url, {
      method: 'GET',
      headers: { ...this.defaultHeaders, ...options?.headers },
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json() as Promise<T>;
  }
}

Теперь вызов client.get<User>('/users/1') вернёт типизированный объект User. Это уже намного лучше, чем работа с any.

Продвинутая обработка ошибок и типизация ответов

Простое выбрасывание ошибки — не лучший подход. Нам нужно различать ошибки сети, ошибки сервера (4xx, 5xx) и ошибки парсинга JSON. Создадим собственные классы ошибок.

Создание классов ошибок

class HttpError extends Error {
  status: number;
  body: any;

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

class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ParseError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ParseError';
  }
}

Улучшенная обработка в методе get

async get<T>(path: string, options?: RequestOptions): Promise<T> {
  try {
    const url = this.buildUrl(path, options?.params);
    const response = await fetch(url, {
      method: 'GET',
      headers: { ...this.defaultHeaders, ...options?.headers },
    });

    let body: any;
    try {
      body = await response.json();
    } catch {
      throw new ParseError('Failed to parse response as JSON');
    }

    if (!response.ok) {
      throw new HttpError(
        `Request failed with status ${response.status}`,
        response.status,
        body
      );
    }

    return body as T;
  } catch (error) {
    if (error instanceof HttpError || error instanceof ParseError) {
      throw error;
    }
    if (error instanceof TypeError) {
      throw new NetworkError('Network request failed');
    }
    throw error;
  }
}

Теперь на стороне вызывающего кода мы можем обрабатывать ошибки разного типа:

try {
  const user = await client.get<User>('/users/1');
} catch (error) {
  if (error instanceof HttpError) {
    console.error(`Server error ${error.status}:`, error.body);
  } else if (error instanceof NetworkError) {
    console.error('Network issue:', error.message);
  } else if (error instanceof ParseError) {
    console.error('Invalid response format:', error.message);
  }
}

Middleware и перехватчики: гибкость на каждом этапе запроса

В реальных приложениях часто требуется добавить логирование, авторизацию, кэширование или повторную отправку запроса при ошибке. Для этого идеально подходит паттерн Middleware (или перехватчики).

Реализация цепочки middleware

Определим интерфейс middleware и добавим поддержку в наш класс.

type NextFunction = (request: RequestInfo) => Promise<Response>;

interface Middleware {
  handle(request: RequestInfo, next: NextFunction): Promise<Response>;
}

class HttpClientWithMiddleware {
  private baseUrl: string;
  private defaultHeaders: Record<string, string>;
  private middlewares: Middleware[] = [];

  constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
    this.baseUrl = baseUrl;
    this.defaultHeaders = defaultHeaders;
  }

  use(middleware: Middleware) {
    this.middlewares.push(middleware);
  }

  private async executeWithMiddleware(request: RequestInfo): Promise<Response> {
    const chain = this.middlewares.reduceRight(
      (next, middleware) => () => middleware.handle(request, next),
      () => fetch(request)
    );
    return chain();
  }

  async get<T>(path: string, options?: RequestOptions): Promise<T> {
    const url = this.buildUrl(path, options?.params);
    const request = new Request(url, {
      method: 'GET',
      headers: { ...this.defaultHeaders, ...options?.headers },
    });

    const response = await this.executeWithMiddleware(request);
    // ... обработка ответа как в предыдущем примере
  }
}

Примеры middleware