Эта статья — о том, как работать с API в JavaScript fetch и axios так, чтобы сеть не диктовала правила. Показаны принципы выбора инструмента, управление ошибками и таймингом, авторизация, отмена и повтор запросов, стриминг и кеширование, плюс рабочая архитектура клиентского слоя.
В сети всё кажется простым, пока не приходит первый тайм-аут, пока токен не протухает в разгар сессии, пока загрузка на 50 МБ не встаёт на 99%. Встроенный fetch даёт чистую основу, Axios — гибкий набор рычагов. Выбор — не спор вкусов, а следствие требований: среда выполнения, масштаб, наблюдаемость, контроль ошибок.
Надёжный клиент — это не энциклопедия методов, а стенд с отлаженной механикой: единые заголовки и сериализация, предсказуемые ошибки, аккуратное обращение с токенами, разумные ретраи и бережное отношение к сети. Дальше — разбор этого стенда по винтам, с образцами кода и подсказками из практики.
Что выбрать для реальных задач: встроенный fetch или библиотеку Axios
Выбирать стоит по задачам: fetch — минимализм и контроль; Axios — готовые удобства и согласованная обработка. При сложной интеграции Axios ускоряет разработку, а fetch остаётся лёгкой и нативной базой.
Для интерфейса, где нужен единый конвейер заголовков, перехватчиков, таймаутов и ошибок, Axios снимает рутину и сокращает код. В проектах с упором на малый размер бандла и тонкую настройку протокольных деталей лучше стартовать с fetch и поверх него строить тонкий слой-адаптер. В браузере fetch уже везде; в Node.js он нативный, начиная с версии 18. Axios же одинаково предсказуем и в браузере, и в Node, что удобно для изоморфного кода. Там, где важно строго контролировать поведение для нестандартных сценариев (стриминг, тонкий контроль памяти, особые кеш-политики), fetch даёт прямой доступ к примитивам. Там же, где ценится возможность быстро собрать «рабочую станцию» с перехватчиками, таймаутами, сериализацией и прослойками — Axios экономит часы и снижает вариативность ошибок.
Когда хватит fetch без дополнительных библиотек
Если нужны базовые запросы и ясная механика без лишнего веса, fetch достаточно. Добавление AbortController и пары утилит покрывает таймауты, отмену и проверку статусов.
Небольшие приложения и микрофронтенды выигрывают от нативности. Контроль статуса делается заведомо явным: разработчик сам решает, что считать ошибкой, как разбирать тело, какие статусы ретраить. Это полезно в командах, где строгая политика кода важнее быстрой сборки из готовых блоков.
Где Axios экономит часы и нервы
Там, где нужна согласованная обработка: единые заголовки, токены, интерцепторы, автоматическая сериализация JSON и встроенный таймаут. Также Axios сразу даёт предсказуемую модель ошибок.
В больших фронтендах, где десятки эндпоинтов и разные команды, Axios становится каркасом: одна точка настройки, один стиль ошибок, один фильтр логирования. Это не снимает ответственность, но гасит случайный разнобой, когда каждый модуль начинает «варить свою кашу».
Сравнение ключевых особенностей
Ниже — сводка, которая помогает быстро решить, кто за что отвечает, и где минимальная настройка упрётся в необходимость доп. кода.
| Критерий | fetch | Axios |
|---|---|---|
| Размер/зависимости | Нулевой, встроен | Отдельная библиотека |
| Ошибки по статусам | Не бросает для 4xx/5xx по умолчанию | Отклоняет промис вне 2xx |
| Таймаут | Только через AbortController | Опция timeout из коробки |
| Интерцепторы | Писать вручную | Встроены (request/response) |
| Сериализация JSON | Ручная (JSON.stringify/parse) | Автоматическая при нужных заголовках |
| Стриминг | WHATWG Streams из коробки | Ограниченно в браузере, богаче в Node |
| Отмена | AbortController | AbortController (v1+) |
На практике уместно закрепить выбор паттерном: единый слой-адаптер поверх fetch или единый клиент на Axios, чтобы не смешивать подходы по проекту. Стабильность важнее локальных выигрышей в одной функции.
Ошибки, статусы и таймауты: предсказуемая обработка без сюрпризов
Предсказуемость достигается договорённостью: где граница ошибки, как обрабатываются статусы и что считается тайм-аутом. Axios делает это из коробки, fetch — через явный код.
В fetch промис не отклоняется на 404 или 500 — только на сетевую ошибку. Поэтому нужна явная проверка response.ok и собственная ошибка с кодом статуса. Тайм-ауты организуются через AbortController. В Axios тайм-аут — опция, а статусы вне 2xx приводят к отклонению промиса, что упрощает централизованную обработку. В обоих случаях стоит договориться о типе прикладной ошибки: единая форма помогает логированию и наблюдаемости. Когда правило фиксировано, следующий шаг — карта действий по статусам, которая закрывает типовые сценарии: обновить токен, показать доступ, ретраить, сохранить fallback.
Шаблоны кода для fetch и Axios
Пара коротких фрагментов кода помогает уложить механику в ровные рельсы и избавляет от «ручных исключений» в каждом вызове.
// fetch: проверка статуса и таймаут через AbortController
async function getJson(url, options = {}, timeout = 8000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
}
return await res.json();
} finally {
clearTimeout(id);
}
}
// Axios: таймаут и предсказуемая ошибка по умолчанию
import axios from 'axios';
const api = axios.create({ baseURL: '/api', timeout: 8000 });
api.interceptors.response.use(
r => r,
e => {
// Добавить нормализацию ошибок, трассировку, метрики
return Promise.reject(e);
}
);
async function getJson(path, config = {}) {
const { data } = await api.get(path, config);
return data;
}
Карта действий по статусам
Сводная таблица помогает держать политику ошибок в одной точке и быстро согласовывать поведение с бэкендом и продуктом.
| Статус | Смысл | Рекомендуемое действие на клиенте |
|---|---|---|
| 400 | Ошибочные данные запроса | Показать подсказку валидации, не ретраить |
| 401 | Неавторизован | Обновить токен, перенаправить на вход |
| 403 | Нет прав | Пояснить ограничения, не ретраить |
| 404 | Не найдено | Показать плейсхолдер/переход, не ретраить |
| 409 | Конфликт версий/состояния | Обновить данные, предложить повтор слиния |
| 429 | Слишком много запросов | Ждать по Retry-After, экспоненциальный backoff |
| 5xx | Ошибка сервера | Короткий ретрай с backoff, включить «вилки» защиты |
Минимальный набор правил для «чистой» ошибки
Договорённость работает, когда простые правила соблюдаются неизменно. Это снимает пласт случайных багов на ровном месте.
- Всегда нормализовать ошибку до единого типа с кодом и сообщением.
- Разделять сетевую ошибку, тайм-аут и прикладную ошибку со статусом.
- Логировать только безопасные поля, не писать токены и персональные данные.
- Для ретраев: проверять идемпотентность и наличие Retry-After/идентификаторов.
- Беречь главный поток: не показывать пользователю «стек трейс», а давать ясный текст.
Аутентификация, куки и CORS: как пройти через пограничные посты
Токен, куки и политика CORS требуют согласованных настроек на клиенте и сервере. Ошибки здесь не терпят догадок — всё должно быть явно.
При работе с токенами Bearer заголовок Authorization лучше прятать в едином клиенте: интерцептор Axios или обёртка над fetch. Для куки на междоменных запросах понадобятся скоординированные параметры: credentials: ‘include’ у fetch и withCredentials у Axios, а на сервере — правильные заголовки CORS (Access-Control-Allow-Credentials, точный Origin, а не звёздочка). CSRF/XSRF-заголовок стоит формировать централизованно, бережно обращаясь с хранилищем: localStorage прост, но уязвим для XSS; httpOnly-куки безопаснее для токена обновления. И наконец, междоменные preflight-запросы уменьшаются предсказуемой политикой заголовков и использованием простых методов там, где возможно.
Настройки для токенов и сессий
Централизация — половина защиты. Вторая половина — бережное хранение и освежение токенов.
// fetch с токеном и куки
async function securedFetch(url, opts = {}) {
const token = sessionStorage.getItem('access_token'); // пример; лучше хранить аккуратнее
const headers = { ...(opts.headers || {}), ...(token ? { Authorization: `Bearer ${token}` } : {}) };
return fetch(url, { ...opts, headers, credentials: 'include' });
}
// Axios с токеном и withCredentials
const api = axios.create({ baseURL: '/api', withCredentials: true });
api.interceptors.request.use((config) => {
const token = sessionStorage.getItem('access_token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
Таблица ключевых заголовков и настроек
Параметры ниже — частая точка рассинхронизации между фронтом и бэком. Их стоит проверить в первую очередь.
| Что | Где | Настройка | Комментарий |
|---|---|---|---|
| Authorization | Клиент | Bearer <token> | Добавлять централизованно |
| credentials/withCredentials | Клиент | fetch: ‘include’, Axios: true | Для междоменных куки |
| Access-Control-Allow-Credentials | Сервер | true | Нельзя сочетать с ‘*’ в Origin |
| Access-Control-Allow-Origin | Сервер | Точный Origin | Звёздочка ломает куки |
| X-CSRF-Token | Клиент/сервер | Согласованное имя/политика | Особенно для stateful-сессий |
| SameSite/Secure для куки | Сервер | Lax/None; Secure при HTTPS | Влияет на передачу в кросс-домене |
Разнесённая логика авторизации в разных местах — скрытый источник багов. Централизовать всё в одном клиенте означает половину победы над «мистическими 401» и застрявшими сессиями.
Повторные запросы, отмена и очереди: контроль над сетью на фронтенде
Ретраи уместны только для сетевых сбоев и идемпотентных операций; отмена спасает UX и бэкенд от лишней нагрузки. Очереди и лимиты держат приложение в тонусе под нагрузкой.
Экспоненциальный backoff с джиттером стабилизирует повторные попытки при кратковременных сбоях и ответах 429/503. Для POST без идемпотентности лучше избегать ретраев или применять серверные идемпотентные ключи. Отмена через AbortController стала общей для fetch и Axios v1: один контроллер — множество параллельных запросов с групповой отменой. Ограничение конкуренции на уровне клиента (например, не более N одновременных запросов к тяжёлому эндпоинту) помогает не «валить» сервер и держать интерфейс отзывчивым. Интерцепторы Axios удобны для реализации ретраев и для маршрутизации отмены, но тот же эффект достигается и обёрткой над fetch.
Примеры отмены и базового ретрая
Код ниже — безопасное минимальное решение: отмена лишних запросов при вводе и аккуратный ретрай GET-запроса.
// Отмена в fetch
const controller = new AbortController();
const p = fetch('/api/search?q=te', { signal: controller.signal });
// пользователь печатает дальше — отменяем предыдущий
controller.abort();
// Отмена в Axios
const c = new AbortController();
axios.get('/api/search?q=te', { signal: c.signal });
c.abort();
// Простой ретрай с backoff
async function retry(fn, { attempts = 3, base = 300 } = {}) {
for (let i = 0; i < attempts; i++) {
try { return await fn(); }
catch (e) {
const isNetwork = !e.response;
const status = e.response?.status;
const retryable = isNetwork || status === 429 || status === 503;
if (!retryable || i === attempts - 1) throw e;
const jitter = Math.random() * 100;
await new Promise(r => setTimeout(r, base * 2 ** i + jitter));
}
}
}
Когда ретраить, а когда — нет
Граница выглядит так: безопасные методы и временные сбои — да; пользовательские действия, меняющие состояние, — только с мерами предосторожности.
| Случай | Решение | Комментарий |
|---|---|---|
| GET 429/503/NetworkError | Ретрай с backoff | Проверить Retry-After |
| PUT/PATCH идемпотентные | Осторожный ретрай | Следить за версионностью (ETag) |
| POST без идемпотентности | Избегать ретраев | Или использовать идемпотентные ключи |
| 400/403/404 | Не ретраить | Ошибки клиента/прав |
| 401 | Обновить токен | Повтор после re-auth |
- Ограничивать одновременные запросы к тяжёлым API до разумного числа.
- Сливать дубликаты запросов с одинаковыми ключами (де-дупликация).
- Отменять устаревшие запросы из автокомплита и бесконечных списков.
Стриминг, загрузка файлов и прогресс: тяжёлый трафик без просадок
Стриминг через fetch позволяет начинать обработку раньше и экономить память; прогресс загрузки проще в Axios благодаря событию onUploadProgress. В Node доступны более продвинутые сценарии потоков.
Когда ответ крупный, потоковое чтение через ReadableStream даёт возможность «есть по кусочку», не дожидаясь всего тела. Это помогает для чатов, серверного рендеринга фрагментов, больших JSON-коллекций. Прогресс загрузки в браузере нативным fetch отследить сложно: требуется XHR либо сторонние приёмы. Axios держит onUploadProgress и onDownloadProgress (с оговорками браузера), что делает его удобным для форм с файлами. На сервере (Node) и fetch, и Axios могут работать со стримами, но детали реализации отличаются: стоит опираться на документацию среды и учитывать backpressure.
Пример потокового чтения ответа
Ниже — упрощённый пример, который показывает, как разбирать ответ частями, не блокируя интерфейс ожиданием всего тела.
// Потоковое чтение с fetch
const res = await fetch('/api/stream');
const reader = res.body.getReader();
const decoder = new TextDecoder();
let done, value;
while (!done) {
({ done, value } = await reader.read());
if (value) {
const chunk = decoder.decode(value, { stream: true });
// Обработать фрагмент, например, добавить в лог
}
}
Форма с файлами: Axios и прогресс
Axios облегчает показ прогресса загрузки: это важно для UX и для раннего обнаружения «повисших» аплоадов.
const form = new FormData();
form.append('file', fileInput.files[0]);
await axios.post('/upload', form, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
const percent = Math.round((e.loaded * 100) / (e.total || e.loaded));
// Обновить индикатор
}
});
Там, где важна устойчивость, стоит добавить контроль таймаута и возобновления загрузок: разбиение на части и серверные «чекпоинты» становятся нормой для больших файлов.
Кэширование и производительность: экономия трафика и скорости реакции
Скорость часто рождается не из «магической оптимизации», а из грамотного кеша: HTTP-заголовки, разумный клиентский слой и предотвращение дубликатов запросов.
Если сервер отдаёт Cache-Control/ETag/Last-Modified, браузерный кеш уже делает большую часть работы. Для fetch можно указать политику cache (no-store, reload, force-cache, only-if-cached), но важнее — совместная политика с бэком: короткий max-age там, где данные часто меняются, и stale-while-revalidate для плавности. На клиенте уровень запросов стоит дополнять локальным кешем по ключу (URL+параметры) с временем жизни. Axios допускает адаптеры и простые кэширующие прослойки на уровне интерцепторов. Де-дупликация (в духе «одинаковые запросы — одна промис-ссылка для всех ожидателей») предотвращает «шторм» в моменты массовых ререндеров.
Простые правила эффективного кеша
Несколько целевых правил дают предсказуемое ускорение без коллекции «вечных» багов с устаревшими данными.
- Соглашение по ключу кеша: URL + сортированные параметры + версия схемы.
- ТTL на клиенте меньше, чем max-age на сервере, чтобы оставаться послушным к инвалидации.
- Stale-while-revalidate: сначала быстрые данные, потом незаметное обновление.
- Де-дупликация параллельных запросов для одного ключа.
- Принудительный обход кеша для критических операций (no-store при изменениях).
Подбор кеш-политики под сценарий
Сценарий определяет политику: для редко меняющихся справочников уместен долгий кеш; для счетчиков — краткий или вовсе отключённый.
| Сценарий | HTTP-политика | Клиентский слой |
|---|---|---|
| Справочники | Cache-Control: max-age=86400, stale-while-revalidate=3600 | Локальный кеш на сутки с «тихим» обновлением |
| Лента событий | Короткий max-age, ETag | Де-дупликация, удержание последней страницы |
| Аналитика/метрики | no-store | Без кеша; контролируемая очередь запросов |
Для углубления темы уместно рассмотреть архитектуру с сервис-воркерами и фоновой синхронизацией: это уже уровень платформы, но и базовая дисциплина заголовков даёт впечатляющий эффект.
Архитектура клиентского слоя: слой-адаптер вместо зоопарка вызовов
Единый клиентский слой — способ превратить десятки нечётких вызовов в один понятный контракт. Это снижает когнитивную нагрузку и делает поведение сети предсказуемым.
Архитектура держится на идее адаптера: выбранный транспорт (fetch или Axios) запирается за модулем, который обеспечивает общие правила — базовый URL, сериализацию, заголовки, политику ошибок, логирование, ретраи, троттлинг и отмену. Поверх строится «каталог» методов домена: user.getProfile(), billing.payInvoice(), search.suggest(). Доменные функции не знают ни про заголовки, ни про токены — только про параметры и результат. Это особенно ценно в больших командах, где свобода выбора в каждом компоненте умножает хаос. Центральный клиент также удобно интегрировать с наблюдаемостью: счётчики статусов, тайминги, метки ретраев.
Скелет универсального клиента
Ниже — схематичный пример, который показывает опорные точки: политика ошибок, токены, ретраи, кеш и типизация.
// Пример: адаптер поверх fetch (упрощённо)
export class ApiClient {
constructor({ baseURL, getToken, ttl = 0 }) {
this.baseURL = baseURL;
this.getToken = getToken;
this.cache = new Map();
this.ttl = ttl;
}
async request(path, { method = 'GET', body, headers = {}, cacheKey, timeout = 8000 } = {}) {
const url = this.baseURL + path;
const key = cacheKey || (method === 'GET' ? url : null);
if (key) {
const hit = this.cache.get(key);
if (hit && hit.expire > Date.now()) return hit.data;
}
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const token = await this.getToken?.();
const res = await fetch(url, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
credentials: 'include'
}).finally(() => clearTimeout(id));
if (!res.ok) {
const text = await res.text().catch(() => '');
const err = new Error(`HTTP ${res.status}`);
err.status = res.status;
err.body = text;
throw err;
}
const data = await res.json().catch(() => null);
if (key && this.ttl) this.cache.set(key, { data, expire: Date.now() + this.ttl });
return data;
}
}
Такой модуль легко заменить на аналогичный, основанный на Axios, сохранив общий контракт. Это и есть ценность адаптера: смена транспорта не ломает доменные вызовы. В качестве дальнейшего чтения полезны материалы по асинхронным паттернам и типизации, например о различиях Promise и async/await (см. сравнение подходов к асинхронности) или руководства по HTTP-кешированию на практике. Для проектирования слоя — заметки о паттерне API-адаптера.
FAQ: ответы на частые вопросы
Что лучше для SSR/Next.js — fetch или Axios?
Оба инструмента работают, но нативный fetch стал стандартом в средах на базе Node 18+, а Next.js тесно интегрирует его в свои API. Axios остаётся удобным, когда нужен единый клиент с интерцепторами и собственным таймаутом. В SSR важно учитывать, что куки и заголовки приходят из контекста запроса, поэтому клиент должен принимать их явно, не полагаясь на состояние процесса.
Для гомогенности и меньшего веса выбирается fetch; для согласованной обработки и повторяемости между сервером и браузером — Axios с аккуратной инъекцией контекста запроса.
Как сделать таймаут для fetch без сторонних библиотек?
Нужно использовать AbortController и setTimeout. Запрос получают сигнал, который прерывает операцию; таймер очищается при завершении.
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 7000);
try {
const res = await fetch(url, { signal: controller.signal });
// обработка res
} finally {
clearTimeout(id);
}
Этот приём универсален и позволяет делать как жёсткие таймауты, так и групповые отмены нескольких запросов одним контроллером.
Как типизировать ответы API в TypeScript?
Определить интерфейсы для DTO и применять их при парсинге и возврате функции-клиента. В Axios можно параметризовать дженериком.
interface User { id: string; name: string; }
async function getUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('HTTP ' + res.status);
return (await res.json()) as User;
}
// Axios
async function getUserA(id: string) {
const { data } = await api.get<User>(`/api/users/${id}`);
return data;
}
Типы стоит хранить рядом с доменными методами, а не распылять по компонентам интерфейса: так лучше контролируется эволюция контракта.
Как отправить FormData и файлы правильно?
В FormData не нужно задавать Content-Type вручную — браузер проставит boundary. В Axios при передаче FormData также достаточно не трогать заголовок.
const fd = new FormData();
fd.append('avatar', file);
await fetch('/upload', { method: 'POST', body: fd });
await axios.post('/upload', fd);
Для больших файлов полезны чанки и возобновление, а для UX — показ прогресса. В Axios onUploadProgress решает задачу из коробки, в fetch пригодится XHR или API ввода/вывода платформы.
Где хранить токены доступа безопаснее?
Лучше избегать localStorage для долгоживущих токенов: он уязвим для XSS. Золотой стандарт — httpOnly-куки для refresh-токена и хранение access-токена в памяти процесса, обновление — по защищённому маршруту.
Если архитектура вынуждает хранить токен в web-хранилище, строго ограничить время жизни, домен и область видимости, а также минимизировать воздействие XSS через Content Security Policy и защиту от инъекций.
Почему fetch не бросает ошибку на 404 и 500?
Это намеренное поведение: fetch отделяет сетевые сбои от HTTP-статусов. Разработчик сам решает, какие статусы считать ошибкой. Такой дизайн даёт большую гибкость, но требует явной проверки ok и генерации прикладной ошибки с кодом статуса.
В Axios эта проверка встроена в библиотеку, поэтому промис отклоняется автоматом на 4xx/5xx — поведение проще для большинства интерфейсов.
Как корректно тестировать сетевые вызовы?
Лучший подход — изоляция уровня транспорта и мок сервера. Для fetch и Axios доступны утилиты, имитирующие ответы. Контрактные тесты проверяют соответствие схемам, а e2e — поведение «от клика до ответа».
Важно тестировать не только «зелёные» ветки, но и ошибки, таймауты, отмены и ретраи. Отдельное внимание — к политике статусов: карта действий должна подтверждаться тестами.
Финальный аккорд: устойчивый клиент к любому API
Стабильный сетевой слой — это не выбор единственного «правильного» инструмента, а дисциплина: общие правила, единый адаптер и уважение к протоколу. Встроенный fetch предоставляет чистые механизмы и тонкий контроль; Axios берёт на себя рутину и выстраивает предсказуемость. Там, где проекту важна масса удобств и скорость разработки, выигрывает библиотека. Там, где нужен минимализм и прозрачность каждого шага, побеждает нативность.
Рабочий маршрут выглядит так: определить политику ошибок и таймаутов, централизовать авторизацию и заголовки, настроить ретраи там, где они безопасны, включить отмену и разумные лимиты, подумать о кешировании и наблюдаемости. Когда этот каркас собран, выбор транспорта перестаёт быть спором убеждений и становится частной деталью реализации.
- Выбрать транспорт: Axios для каркаса «из коробки», fetch для нативного минимализма.
- Собрать единый клиент: базовый URL, заголовки, токены, политика ошибок и таймаутов.
- Включить отмену через AbortController; настроить очереди и де-дупликацию.
- Определить карту статусов и ретраев с backoff и уважением к идемпотентности.
- Настроить кеш: HTTP-заголовки, локальный TTL, stale-while-revalidate.
- Добавить наблюдаемость: тайминги, счётчики статусов, трассировку проблемных эндпоинтов.
Этот порядок действий превращает набор разрозненных вызовов в согласованный механизм. И тогда сеть перестаёт быть капризным собеседником, а становится добросовестным проводником данных — тихим и быстрым.

