Навигация

HTTP-клиенты: fetch, axios, ky и ofetch

В любом фронтенд-проекте рано или поздно появляется слой для общения с сервером. Голый fetch подходит для простых задач, но по мере роста проекта нужны базовый URL, interceptors, обработка ошибок и отмена запросов. Выбор HTTP-клиента влияет на архитектуру всего приложения.

Содержание

  1. Зачем отдельный HTTP-клиент
  2. fetch — нативный API браузера
  3. axios — классическая библиотека
  4. ky — лёгкий обёрточный клиент
  5. ofetch — универсальный клиент от экосистемы Nuxt
  6. Сравнение библиотек
  7. Когда что использовать
  8. Конфигурация через переменные окружения
  9. Паттерн централизованного API-клиента
  10. Лучшие практики
  11. Антипаттерны

Зачем отдельный HTTP-клиент

В любом фронтенд-приложении рано или поздно появляется слой, отвечающий за общение с сервером. На первых порах можно обходиться голым fetch, но по мере роста проекта требуется:

  • Базовый URL — не повторять https://api.example.com в каждом запросе.
  • Заголовки по умолчанию — Content-Type, Authorization, Accept-Language.
  • Обработка ошибок — единообразное превращение HTTP-ответов в понятные исключения.
  • Interceptors — логирование, токены, request ID, retry.
  • Отмена запросов — AbortController или встроенные механизмы библиотеки.
  • Типизация — удобная работа с TypeScript.

Хороший HTTP-клиент решает эти задачи без самописной обёртки над fetch.


fetch — нативный API браузера

fetch — встроенный в браузер и Node.js 18+ API для HTTP-запросов. Он низкоуровневый, стандартизированный и не требует установки.

Базовый пример

const response = await fetch('/api/users');

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

const users = await response.json();

Преимущества

  • Встроен в браузер и современный Node.js.
  • Поддерживает AbortController для отмены.
  • Работает со Streams и FormData.
  • Нет лишних килобайтов в бандле.

Недостатки

  • Не бросает исключение на HTTP-ошибках (4xx, 5xx) — нужно проверять response.ok вручную.
  • Нет встроенных interceptors.
  • Нет автоматического преобразования тела запроса в JSON.
  • Более многословный API.
  • Отсутствие встроенного retry и timeout.

fetch с POST и JSON

const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' }),
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.message);
}

const user = await response.json();

fetch с AbortController

const controller = new AbortController();

const response = await fetch('/api/users', {
  signal: controller.signal,
});

// Отмена через 5 секунд
setTimeout(() => controller.abort(), 5000);

axios — классическая библиотека

axios — самая популярная HTTP-библиотека для JavaScript. Она работает и в браузере, и в Node.js, имеет удобный API и богатую экосистему.

Базовый пример

import axios from 'axios';

const { data: users } = await axios.get('/api/users');

Преимущества

  • Автоматическая сериализация JSON.
  • Бросает исключение на status >= 400.
  • Interceptors для запросов и ответов.
  • Поддержка timeout из коробки.
  • Поддержка transformRequest / transformResponse.
  • Старые, но знакомые CancelToken и современный AbortController.
  • Отличная поддержка старых браузеров (XMLHttpRequest под капотом).

Недостатки

  • Размер бандла (~13 KB gzipped).
  • Некоторые фичи устарели (CancelToken).
  • В 2024–2026 годах сообщество активно обсуждает, стоит ли использовать axios в новых проектах, потому что fetch покрывает большинство сценариев.

axios instance

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

const { data: user } = await api.get('/users/123');

Interceptors

api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

Отмена запроса в axios

const controller = new AbortController();

const { data } = await axios.get('/api/users', {
  signal: controller.signal,
});

controller.abort();

ky — лёгкий обёрточный клиент

ky — это небольшая обёртка над fetch от создателя got. Сохраняет всё хорошее от fetch, но добавляет удобство axios.

Базовый пример

import ky from 'ky';

const users = await ky.get('/api/users').json();

Преимущества

  • Маленький размер (~4 KB gzipped).
  • Бросает исключение на HTTP-ошибках.
  • Автоматический .json() через метод .json().
  • Удобный API для retry, timeout, hooks.
  • Поддержка AbortController.
  • Написан на fetch, поэтому использует нативные возможности браузера.

Недостатки

  • Нет поддержки старых браузеров без fetch.
  • Экосистема плагинов меньше, чем у axios.
  • Некоторые привычные фичи axios (например, transformResponse) отсутствуют.

ky instance

const api = ky.create({
  prefixUrl: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
  retry: {
    limit: 3,
    methods: ['get'],
    statusCodes: [408, 413, 429, 500, 502, 503, 504],
  },
});

const user = await api.get('users/123').json();

Hooks в ky

const api = ky.create({
  hooks: {
    beforeRequest: [
      (request) => {
        request.headers.set('Authorization', `Bearer ${getToken()}`);
      },
    ],
    afterResponse: [
      async (request, options, response) => {
        if (!response.ok) {
          console.error('API error:', response.status);
        }
        return response;
      },
    ],
  },
});

Отмена в ky

const controller = new AbortController();

const users = await ky.get('/api/users', {
  signal: controller.signal,
}).json();

ofetch — универсальный клиент от экосистемы Nuxt

ofetch (ранее $fetch в Nuxt 2/3) — HTTP-клиент от UnJS, используемый в Nuxt 3. Он основан на fetch и заточен под SSR/CSR изоморфные запросы.

Базовый пример

import { ofetch } from 'ofetch';

const users = await ofetch('/api/users');

Преимущества

  • Работает одинаково в браузере и на сервере (SSR).
  • Автоматический парсинг JSON.
  • Встроенный retry.
  • Удобная обработка ошибок через onRequest, onResponse, onRequestError, onResponseError.
  • Поддержка baseURL.

Недостатки

  • Тесно связан с экосистемой UnJS/Nuxt.
  • Меньше документации и примеров вне Nuxt.
  • Не всегда нужен в React/Next.js проектах.

ofetch instance

const api = ofetch.create({
  baseURL: 'https://api.example.com',
  retry: 3,
  headers: {
    Accept: 'application/json',
  },
  async onRequest({ options }) {
    options.headers.set('Authorization', `Bearer ${getToken()}`);
  },
  async onResponseError({ response }) {
    if (response.status === 401) {
      await refreshToken();
    }
  },
});

const user = await api('/users/123');

Сравнение библиотек

Критерийfetchaxioskyofetch
Размер0 KB~13 KB~4 KB~5 KB
БазаНативный APIXMLHttpRequestfetchfetch
JSONРучной .json()АвтоМетод .json()Авто
Ошибки HTTPНе бросаетБросаетБросаетБросает
InterceptorsНетДаHooksHooks
RetryНетНетДаДа
TimeoutНетДаДаДа
AbortControllerДаДаДаДа
Старые браузерыЗависит от браузераДаНетНет
TypeScriptСреднеХорошоХорошоХорошо
SSRДаДаДаОтлично

Когда что использовать

Выбирай fetch, если:

  • Проект маленький и запросов мало.
  • Важен размер бандла (0 KB).
  • Не нужны interceptors и retry.
  • Вы готовы писать небольшую обёртку самостоятельно.

Выбирай axios, если:

  • Команда уже знакома с axios.
  • Нужна поддержка старых браузеров.
  • Используются сложные interceptors.
  • Проект большой и миграция на fetch-обёртку нецелесообразна.

Выбирай ky, если:

  • Хочется лёгкую альтернативу axios.
  • Важен размер бандла.
  • Целевые браузеры поддерживают fetch.
  • Нужен встроенный retry.

Выбирай ofetch, если:

  • Проект на Nuxt 3.
  • Нужны изоморфные запросы (SSR + CSR).
  • Хочется минимальной конфигурации.

Конфигурация через переменные окружения

Адреса API не должны быть захардкожены в коде. Для разных окружений — development, staging, production — используются разные baseURL. Хранить их принято в переменных окружения.

Префиксы для популярных фреймворков

Фреймворк / сборщикПрефикс для клиентских переменныхПример
ViteVITE_VITE_API_URL
Next.jsNEXT_PUBLIC_NEXT_PUBLIC_API_URL
NuxtNUXT_PUBLIC_NUXT_PUBLIC_API_BASE
Create React AppREACT_APP_REACT_APP_API_URL
Vanilla webpackнастраивается вручнуюAPI_URL

Пример .env

# .env.local
VITE_API_URL=https://api.example.com

Использование в API-клиенте

import ky from 'ky';

export const api = ky.create({
  prefixUrl: import.meta.env.VITE_API_URL,
  timeout: 10000,
});

Важное правило: публичные vs секретные переменные

Переменные с публичными префиксами (NEXT_PUBLIC_, VITE_, NUXT_PUBLIC_, REACT_APP_) встраиваются в клиентский бандл на этапе сборки. Любой пользователь может увидеть их значение в DevTools.

# ✅ Можно использовать на клиенте
NEXT_PUBLIC_API_URL=https://api.example.com

# ❌ Никогда не делайте так
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc123

Секретные ключи должны оставаться на сервере. Подробнее о безопасности env-переменных — в разделе Управление секретами.

.env не должен попадать в git

# .gitignore
.env
.env.local
.env.*.local

Если .env случайно попал в репозиторий, считайте все значения скомпрометированными — отзовите и пересоздайте ключи.


Паттерн централизованного API-клиента

Независимо от библиотеки, рекомендуется создавать единый экземпляр клиента для всего приложения.

На ky

import ky from 'ky';

export const api = ky.create({
  prefixUrl: import.meta.env.VITE_API_URL,
  timeout: 10000,
  retry: {
    limit: 2,
    methods: ['get'],
    statusCodes: [408, 429, 500, 502, 503, 504],
  },
  hooks: {
    beforeRequest: [
      (request) => {
        const token = localStorage.getItem('accessToken');
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
        request.headers.set('X-Request-ID', crypto.randomUUID());
      },
    ],
    afterResponse: [
      async (request, options, response) => {
        if (!response.ok) {
          const body = await response.json().catch(() => ({}));
          throw new ApiError(response.status, body.message || 'API error');
        }
        return response;
      },
    ],
  },
});

class ApiError extends Error {
  constructor(
    public status: number,
    public message: string
  ) {
    super(message);
  }
}

Использование в компонентах

import { api } from '@/api/client';

async function loadUser(id: string) {
  return api.get(`users/${id}`).json<User>();
}

Лучшие практики

1. Создавай один экземпляр клиента

Не импортируй axios или ky напрямую в каждом файле. Используй настроенный инстанс.

// ❌ Плохо
import axios from 'axios';
await axios.get('/api/users');

// ✅ Хорошо
import { api } from '@/api/client';
await api.get('users');

2. Централизуй обработку ошибок

Превращайте HTTP-ошибки в понятные исключения на уровне клиента, а не в каждом компоненте.

3. Используй timeout

Бесконечно висящий запрос хуже упавшего.

const api = ky.create({
  timeout: 10000,
});

4. Логируй запросы в dev-режиме

api.interceptors.request.use((config) => {
  if (import.meta.env.DEV) {
    console.log('→', config.method?.toUpperCase(), config.url);
  }
  return config;
});

5. Добавляй request ID

Уникальный ID на каждый запрос упрощает отладку на бэкенде.

request.headers.set('X-Request-ID', crypto.randomUUID());

6. Не храни токены в небезопасных местах

Используй HttpOnly cookies там, где возможно. Если используешь localStorage, понимай риски XSS.


Антипаттерны

1. Дублирование baseURL

// ❌ Плохо
await fetch('https://api.example.com/users');
await fetch('https://api.example.com/posts');

// ✅ Хорошо
await api.get('users');
await api.get('posts');

2. Игнорирование HTTP-ошибок с fetch

// ❌ Плохо
const response = await fetch('/api/users');
const data = await response.json(); // Может быть 500

// ✅ Хорошо
if (!response.ok) throw new Error(...);

3. Хардкод статусов по всему проекту

// ❌ Плохо
if (error.response.status === 401) { ... }

// ✅ Хорошо
if (error.status === HttpStatus.Unauthorized) { ... }

4. Перехват всех ошибок в interceptor

// ❌ Плохо: тихо проглатываем ошибки
api.interceptors.response.use(
  (res) => res,
  () => null
);

5. Разный клиент в разных частях приложения

Использование axios в одних модулях, fetch в других и ky в третьих усложняет поддержку.


Ключевые тезисы для интервью

  • fetch — нативный API, не бросает исключение на HTTP-ошибках и не имеет interceptors.
  • axios бросает исключение на status >= 400, поддерживает interceptors и timeout из коробки.
  • ky — лёгкая обёртка над fetch (~4 KB) с автоматическим .json() и встроенным retry.
  • ofetch заточен под SSR/CSR изоморфные запросы и используется в Nuxt 3.
  • Централизованный API-клиент — единый экземпляр с baseURL, timeout, hooks и обработкой ошибок.
  • Interceptors добавляют токен авторизации, request ID и логируют запросы в dev-режиме.
  • Переменные окружения с публичными префиксами (VITE_, NEXT_PUBLIC_) попадают в клиентский бандл.
  • Секретные ключи никогда не должны быть в клиентском бандле.
  • Антипаттерны: дублирование baseURL, игнорирование HTTP-ошибок, разные клиенты в разных модулях.

Заключение

  • fetch — база, но требует обёртки для серьёзных проектов.
  • axios — проверенный выбор с богатым API, но тяжелее.
  • ky — лёгкая и современная альтернатива axios.
  • ofetch — лучший выбор для Nuxt/SSR.

Для Middle+ разработчика важно понимать не только синтаксис библиотек, но и когда какую выбирать, как централизовать клиент и как обрабатывать ошибки единообразно.

Полезные ссылки