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

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

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

Начать курс

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

TypeScript — это строго типизированное расширение JavaScript, которое добавляет в веб-разработку надежность и читаемость кода. При работе с внешними API или бэкендом ключевым навыком становится умение правильно выполнять HTTP-запросы. В этой статье мы разберем, как создать полноценный HTTP-клиент на TypeScript, начиная от базовых принципов и заканчивая продвинутыми паттернами обработки ошибок и типизации.

Почему TypeScript идеален для HTTP-клиентов?

JavaScript предлагает несколько способов выполнения HTTP-запросов: встроенный fetch, библиотеки axios и node-fetch. Однако TypeScript позволяет вывести работу с ними на новый уровень благодаря статической типизации. Вы можете заранее описать структуру запроса и ответа, что исключает множество ошибок на этапе компиляции. Представьте, что вы работаете с API, которое возвращает список пользователей. Без типов вы рискуете обратиться к несуществующему полю, и ошибка проявится только в рантайме. TypeScript решает эту проблему.

Основная часть: создаем HTTP-клиент с нуля

1. Базовый GET-запрос с fetch и типизацией

Начнем с самого простого — GET-запроса. В TypeScript мы можем определить интерфейс для ответа и передать его в дженерик-функцию fetch. Вот пример:

interface User {
  id: number;
  name: string;
  email: string;
}

async function getUser(id: number): Promise<User> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  const data: User = await response.json();
  return data;
}

// Использование
getUser(1)
  .then(user => console.log(user.name))
  .catch(error => console.error('Ошибка:', error.message));

Здесь мы явно указываем, что функция возвращает Promise<User>. Если API изменится, компилятор TypeScript предупредит вас о несоответствии типов. Обратите внимание на проверку response.ok — это обязательный шаг для обработки HTTP-ошибок (404, 500 и т.д.).

2. POST-запрос с телом и заголовками

Отправка данных на сервер требует настройки заголовков и сериализации тела запроса. Создадим функцию для создания нового пользователя:

interface CreateUserRequest {
  name: string;
  email: string;
}

interface CreateUserResponse {
  id: number;
  name: string;
  email: string;
}

async function createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
  const response = await fetch('https://jsonplaceholder.typicode.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });

  if (!response.ok) {
    const errorBody = await response.text();
    throw new Error(`Ошибка создания: ${response.status} - ${errorBody}`);
  }

  return await response.json();
}

// Пример вызова
const newUser: CreateUserRequest = { name: 'Иван', email: 'ivan@example.com' };
createUser(newUser).then(result => console.log('Создан ID:', result.id));

Типизация CreateUserRequest и CreateUserResponse делает код самодокументируемым. Другие разработчики сразу видят, какие поля обязательны для отправки и что ожидать в ответе.

3. Универсальный HTTP-клиент с обработкой ошибок

В реальных проектах редко обходятся одной функцией. Лучше создать универсальный класс или набор функций, который централизует логику запросов. Вот пример продвинутого клиента:

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

interface RequestConfig {
  method: HttpMethod;
  endpoint: string;
  body?: unknown;
  params?: Record<string, string>;
}

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

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

  async request<T>(config: RequestConfig): Promise<T> {
    const url = this.buildUrl(config.endpoint, config.params);
    
    const options: RequestInit = {
      method: config.method,
      headers: {
        'Content-Type': 'application/json',
      },
    };

    if (config.body && config.method !== 'GET') {
      options.body = JSON.stringify(config.body);
    }

    const response = await fetch(url, options);

    if (!response.ok) {
      const errorData = await response.json().catch(() => null);
      throw {
        status: response.status,
        message: response.statusText,
        data: errorData,
      };
    }

    return await response.json() as T;
  }

  async get<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
    return this.request<T>({ method: 'GET', endpoint, params });
  }

  async post<T>(endpoint: string, body: unknown): Promise<T> {
    return this.request<T>({ method: 'POST', endpoint, body });
  }
}

// Использование
const api = new HttpClient('https://api.example.com');

interface Product {
  id: number;
  title: string;
  price: number;
}

// GET с параметрами
api.get<Product[]>('/products', { category: 'electronics' })
  .then(products => console.log(products[0].title))
  .catch(err => console.error(`Ошибка ${err.status}: ${err.message}`));

// POST
api.post<Product>('/products', { title: 'Новый товар', price: 100 })
  .then(product => console.log(product.id));

Этот клиент решает несколько задач:

  • Централизованная обработка ошибок — все HTTP-ошибки перехватываются и форматируются в едином стиле.
  • Гибкость — поддержка всех основных методов, параметров запроса и тела.
  • Типобезопасность — дженерик <T> позволяет указать ожидаемый тип ответа для каждого запроса.

4. Расширенная обработка ошибок и тайм-ауты

Иногда сервер зависает или отвечает слишком долго. Добавим тайм-аут и более детальную обработку:

async requestWithTimeout<T>(config: RequestConfig, timeout                

Блоги

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