Каждый туториал по fetch в Node.js учит вас писать await fetch(url) — и на этом всё. А потом ваше продакшен-приложение молча проглатывает ошибку 500, один запрос висит 90 секунд без таймаута, и в пятницу вечером вы отлаживаете то, что должно было быть очевидным.
Я уже какое-то время делаю внутренние инструменты и data pipeline в Thunderbit, и могу сказать так: разрыв между «fetch работает в моём туториале» и «fetch работает в продакшене» — это именно то место, где и живёт большая часть боли. Один разработчик на Reddit очень точно сказал: «когда вы идёте в продакшен, понимаете, что вам нужно что-то более надёжное, чем нативный fetch».
Другой признался: «3 года работал веб-разработчиком и только сегодня узнал, что блок catch в fetch API НЕ для HTTP-ошибок». Это руководство покрывает пять вещей, которые большинство туториалов пропускают: ловушку с ошибками, таймауты через AbortController, логику повторных попыток, повторное использование соединений и момент, когда стоит уйти от fetch к извлечению структурированных данных. Если у вас когда-либо fetch-запрос тихо падал в продакшене — это для вас.

Что такое Node.js Fetch API?
Node.js Fetch API — это встроенный, совместимый с браузером способ отправлять HTTP-запросы (GET, POST, PUT, DELETE и т. д.) из Node.js — без установки Axios, node-fetch или любого другого пакета. Если вы уже использовали fetch() в браузере, синтаксис вам знаком. Теперь тот же API работает и на сервере.
Вот короткая история версий:
| Этап | Версия Node | Что произошло |
|---|---|---|
| Экспериментальный флаг fetch | v17.5.0 / v16.15.0 | fetch добавлен за флагом --experimental-fetch |
| fetch по умолчанию в глобальной области | v18.0.0 | Экспериментальный fetch стал доступен глобально, на базе Undici |
| Стабильный fetch | v21.0.0 | Больше не экспериментальный |
| База для продакшена в 2026 | v22 LTS / v24 LTS | Рекомендуется для продакшена; v20 уже EOL |
Внутри Node fetch работает на Undici — высокопроизводительном HTTP-клиенте, созданном специально для Node.js. Он не опирается на старый встроенный модуль http. Практическая польза в том, что вы получаете современный HTTP API на базе Promise, который одинаково работает в браузерном коде, Express-бэкенде, serverless-функции и CLI-скриптах.
Почему Node.js Fetch API важен для ваших проектов
До Node 18 каждый новый проект начинался с одного и того же ритуала: npm install axios или npm install node-fetch. В 2026 году, если ваш проект работает на поддерживаемой LTS-версии Node, для базовых HTTP-запросов не нужно ни одной зависимости. Это реальный плюс для размера сборки, безопасности цепочки поставок и онбординга — фронтенд- и бэкенд-разработчики наконец работают с одним и тем же API.
Вот где нативный fetch особенно хорош:
| Сценарий | Почему нативный fetch хорош | Оговорка для продакшена |
|---|---|---|
| Express/Fastify-бэкенд вызывает REST API | Знакомый async/await, без зависимости | Добавьте таймаут и проверки response.ok |
| Serverless-функции (Lambda, Vercel и т. д.) | Меньше поверхность cold start, не нужно ставить пакет | Держите таймаут ниже максимального лимита платформы |
| CLI-скрипты и автоматизации | Простой GET/POST без подготовки проекта | Добавьте retry/backoff для нестабильных API |
| Доставка или проксирование webhook | Стандартные HTTP-методы и заголовки | Не пытайтесь слепо повторять неидемпотентные POST |
| Отчёты и дашборды | Удобно забирать JSON из API | Используйте пагинацию и пул соединений в циклах |
| Взаимодействие микросервисов | Подходит для простых внутренних HTTP-вызовов | Рассмотрите Got или напрямую Undici для retry, hooks или HTTP/2 |
Для новых проектов на Node 22+ нативный fetch — разумный выбор по умолчанию, если только вам не нужны функции, которых у него нет (interceptors, встроенный retry, HTTP/2 и т. д.). Числа загрузок npm хорошо показывают, что рынок меняется: node-fetch всё ещё получает около 144,9 млн загрузок в неделю, но большая часть этого — наследие и транзитивные зависимости. Axios — около 108,6 млн, Undici — около 106 млн, Got — около 36 млн, а Ky — около 5,6 млн. Тренд ясен: нативный fetch — новая база, а сторонние клиенты нужны под конкретные задачи.
Нативный fetch vs node-fetch vs Axios vs Got vs Ky: матрица выбора на 2026 год
Самый частый вопрос, который я вижу на форумах разработчиков: «Какой HTTP-клиент мне использовать в Node.js?» Один пользователь Reddit сформулировал это так: «зачем импортировать библиотеку, если в языке/фреймворке эта функциональность уже встроена?» Справедливое замечание — но ответ зависит от ваших задач.

| Функция | Нативный fetch | node-fetch v3 | axios | got v15 | ky v2 |
|---|---|---|---|---|---|
| Версия Node.js | ≥18 (рекомендуются 22/24 LTS) | ≥12.20 | Широкая | ≥22 | ≥22 |
| Нужна установка | Нет | Да | Да | Да | Да |
| Поддержка ESM + CJS | Оба варианта (глобально) | Только ESM (v3) | Оба | Только ESM | Только ESM |
| Авто-ошибка на 4xx/5xx | Нет | Нет | Да | Да | Да |
| Встроенный retry | Нет | Нет | Нет | Да | Да |
| Request interceptors | Нет | Нет | Да | Да (hooks) | Да (hooks) |
| Поддержка streaming | Web ReadableStream | Да | Ограниченная | Сильная поддержка Node streams | На базе fetch |
| Размер установки | 0 KB | ~107 KB, 3 deps | ~2.8 MB, 4 deps | ~355 KB, 12 deps | ~405 KB, 0 deps |
| Поддержка HTTP/2 | Через Undici dispatcher | Нет | Нет | Да | Нет (обёртка над fetch) |
Коротко про проблему ESM/CJS: node-fetch v3 работает только в ESM, и это сломало много проектов, которые использовали require(). Нативный fetch — глобальный, он работает и в CJS, и в ESM-файлах без каких-либо танцев с импортом. Если вы застряли на node-fetch v2 из-за CommonJS, нативный fetch решает проблему полностью.
И насчёт ранних проблем со стабильностью: да, в первой реализации fetch в Node 18 были реальные баги. Один разработчик на Reddit писал: «Недавно наткнулся на жёсткий баг в нативном fetch в Node 18, пришлось переделывать приложение». Это было в 2023 году. В 2026-м, с Node 22 и 24 LTS, эти проблемы уже решены. Нативный fetch готов к продакшену.
Когда стоит остаться на нативном fetch
Используйте нативный fetch, если:
- ваш проект работает на Node 22 LTS или Node 24 LTS;
- запросы — это простые REST-вызовы (GET, POST, PUT, DELETE);
- вы готовы добавить небольшой wrapper для
response.ok, парсинга JSON, таймаутов и повторных попыток; - вам нужна нулевая поверхность зависимостей и меньше рисков цепочки поставок;
- вам важна паритетность API между браузером и сервером;
- вы работаете в serverless или edge-средах, где встроенные API предпочтительнее.
Когда Axios, Got или Ky подходят лучше
Axios — хороший выбор, если ваша команда опирается на interceptors для запросов и ответов, например для автоматического обновления auth-токенов, tenant-заголовков или централизованного логирования, если вам нужно стандартное отклонение на HTTP-ошибках или обратная совместимость со старыми версиями Node.
Got создан для высоконагруженных Node-сервисов, которым нужны встроенные retry, hooks, расширенные фазы таймаута, потоки, helper'ы для пагинации, Unix sockets, proxy/caching workflow или поддержка HTTP/2. Это швейцарский нож для HTTP-задач только в Node.
Ky — золотая середина, если вам нравится простота fetch, но хочется меньше шаблонного кода: он добавляет retry, timeout, hooks и HTTPError в маленьком пакете без зависимостей.
Как делать GET-запросы с Node.js Fetch API
GET-запрос с async/await выглядит так:
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const post = await response.json();
console.log(post.title);
// → "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
А если вам удобнее цепочка .then():
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(post => console.log(post.title))
.catch(error => console.error(error));
Оба варианта работают. Но пока ни один из них не безопасен для продакшена (к этому мы ещё вернёмся).
Режимы чтения ответа, которые нужно знать:
| Метод | Когда использовать |
|---|---|
response.json() | Сервер возвращает JSON |
response.text() | Сервер возвращает HTML, обычный текст, CSV или Markdown |
response.arrayBuffer() | Нужны бинарные данные (изображения, файлы) |
response.body | Нужна потоковая/чанковая обработка |
Лучший паттерн — тот, который действительно проверяет ошибки:
async function getPost(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
return response.json();
}
const post = await getPost(1);
console.log(post.title);
Вот эта строка if (!response.ok) и отделяет туториал от production-кода. И это подводит нас к главной ловушке.
Как отправлять POST-запросы с Node.js Fetch API
POST-запросы устроены так же — просто задайте метод, заголовки и тело:
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Руководство по Node fetch',
body: 'Production fetch требует обработки ошибок.',
userId: 1,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const created = await response.json();
console.log(created.id); // → 101
Отправка других типов запросов (PUT, DELETE, PATCH)
PUT, PATCH и DELETE используют ту же структуру, только со значением method другим:
// PUT — полная замена
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1, title: 'Заменено', body: 'Полная замена', userId: 1 }),
});
// PATCH — частичное обновление
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Частичное обновление' }),
});
// DELETE
await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'DELETE',
});
Ловушка Express body-parser: если вы отправляете JSON на сервер Express, а req.body приходит undefined, почти всегда решение такое: используйте express.json(), а не express.urlencoded(). Серверу нужен middleware express.json() до маршрута, чтобы распарсить тело с Content-Type: application/json. Это один из самых частых вопросов на Stack Overflow про Express, и он подлавливает людей снова и снова.
import express from 'express';
const app = express();
app.use(express.json()); // ← именно это нужно для JSON-тел
app.post('/api/posts', (req, res) => {
res.json({ received: req.body });
});
Ловушка fetch(), которая ломает продакшен-приложения

Именно здесь возникает большинство багов fetch в продакшене.
fetch() не отклоняет Promise при HTTP-ошибках 4xx или 5xx. Он отклоняет его только при сбоях на уровне сети — ошибки DNS, отсутствие интернета, прерванные запросы. Если сервер возвращает 403 Forbidden или 500 Internal Server Error, fetch считает это успешным ответом. Ваш .catch() не сработает. Ваш try/catch этого не поймает. А код спокойно начнёт обрабатывать то, что прислал сервер.
Документация MDN говорит об этом прямо, но большинство туториалов этот момент замалчивают. Итог? Такой код выглядит нормально, но тихо проглатывает ошибки:
try {
const response = await fetch('https://api.example.com/private');
const data = await response.json(); // ← Это выполнится даже при 403
console.log('Выглядит успешно:', data);
} catch (error) {
// Сюда попадут только ошибки на уровне сети
console.error('Поймано:', error);
}
Кратко разберём, что реально ловит каждый паттерн:
| Паттерн | Ловит сетевые ошибки | Ловит 4xx/5xx | Безопасно парсит JSON | Переиспользуемый |
|---|---|---|---|---|
Сырой .then(res => res.json()) | Да (через .catch()) | Нет | Нет проверки content-type | Нет |
try/catch с await fetch() | Да | Нет | Нет проверки content-type | Нет |
Ручной if (!res.ok) в каждом вызове | Да | Да | Зависит от каждого вызова | Частично |
Собственный wrapper fetchJSON() | Да | Да | Да | Да |
Сделайте переиспользуемый wrapper fetchJSON()
Сделайте один wrapper. Импортируйте его везде. Перестаньте копировать if (!response.ok) в каждый файл:
export class HTTPError extends Error {
constructor(message, { status, statusText, url, body }) {
super(message);
this.name = 'HTTPError';
this.status = status;
this.statusText = statusText;
this.url = url;
this.body = body;
}
}
export async function fetchJSON(url, options = {}) {
const response = await fetch(url, {
headers: {
Accept: 'application/json',
...options.headers,
},
...options,
});
const contentType = response.headers.get('content-type') || '';
const isJSON = contentType.includes('application/json');
const body = isJSON ? await response.json().catch(() => null) : await response.text();
if (!response.ok) {
throw new HTTPError(`HTTP ${response.status} ${response.statusText}`, {
status: response.status,
statusText: response.statusText,
url: response.url,
body,
});
}
return body;
}
Теперь, если сервер вернёт 403:
try {
const data = await fetchJSON('https://api.example.com/private');
} catch (error) {
if (error instanceof HTTPError) {
console.error(`Сервер вернул ${error.status}:`, error.body);
} else {
console.error('Сбой сети или другая ошибка:', error);
}
}
Ошибка несёт код статуса, тело ответа и URL — всё, что нужно для логов, алертов или сообщений пользователю. Импортируйте это один раз и используйте везде.
AbortController и таймауты: production-паттерн для Node.js Fetch API

Без таймаута fetch-запрос может висеть бесконечно, если удалённый сервер замолчал. Ваш Express-роут блокируется. Ваша Lambda сжигает весь бюджет выполнения. Ваш скрипт просто... стоит.
Я проверил топ поисковой выдачи: ни один туториал по fetch именно для Node.js не объясняет отмену запросов или таймауты. А ведь таймауты — одна из главных причин, почему разработчики продолжают выбирать Axios или Got. На Reddit буквально есть тред с названием «Node fetch does not timeout».
Использование AbortSignal.timeout() (Node 18.11+)
Самый простой способ — одна дополнительная опция:
try {
const response = await fetch('https://api.example.com/data', {
signal: AbortSignal.timeout(5000), // 5 секунд
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Запрос превысил таймаут в 5 секунд.');
} else {
throw error;
}
}
Важно: AbortSignal.timeout() выбрасывает TimeoutError, а не AbortError. Даже опытные разработчики иногда путают это.
Ручной таймаут через AbortController
Если нужен больший контроль — или если вы хотите отменять запрос не только по таймеру, но и по действию пользователя:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.example.com/data', {
signal: controller.signal,
});
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'AbortError') {
console.error('Запрос был отменён вручную.');
} else {
throw error;
}
} finally {
clearTimeout(timeout);
}
Как различать AbortError и TimeoutError
Это важно для логов и сообщений пользователю:
| Путь отмены | Имя ошибки в блоке catch |
|---|---|
AbortSignal.timeout(ms) | TimeoutError |
controller.abort() | AbortError |
| Ошибка DNS/сети | Обычно TypeError: fetch failed |
Вот практический сценарий: Express-роут вызывает внешний API и должен ответить за 3 секунды:
app.get('/dashboard', async (req, res, next) => {
try {
const data = await fetchJSON('https://api.example.com/report', {
signal: AbortSignal.timeout(3000),
});
res.json(data);
} catch (error) {
if (error.name === 'TimeoutError') {
res.status(504).json({ error: 'Внешний API не успел ответить' });
return;
}
next(error);
}
});
Без этого паттерна медленный upstream API будет блокировать весь маршрут, пока клиент не сдастся.
Логика retry и повторное использование соединений: как сделать Node.js Fetch API production-grade
В нативном fetch нет встроенного retry. Сетевой сбой или временная ошибка 503 — и запрос просто падает. Для большинства операций чтения в продакшене это неприемлемо.
Составляемый retry-wrapper с экспоненциальным backoff
Ниже намеренно коротко — примерно 10 строк рабочей логики:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
export async function fetchWithRetry(url, options = {}, retries = 2) {
for (let attempt = 0; ; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok || ![408, 429, 500, 502, 503, 504].includes(response.status)) {
return response;
}
if (attempt >= retries) return response;
} catch (error) {
if (attempt >= retries) throw error;
}
await wait(250 * 2 ** attempt); // 250 мс, 500 мс, 1000 мс...
}
}
Когда делать retry, а когда не делать
- Делайте retry: идемпотентные GET- и HEAD-запросы, временные статусы (408, 429, 500, 502, 503, 504), кратковременные сетевые сбои.
- Не делайте retry: неидемпотентные POST-запросы, которые создают записи, списывают деньги или запускают сайд-эффекты — если только вы не используете idempotency keys.
- Учитывайте Retry-After: для 429 (rate limit) и 503 (service unavailable) сначала проверьте заголовок
Retry-After, прежде чем делать backoff.
Если вы не хотите писать свою retry-логику, Ky — это лёгкая обёртка над fetch, которая сразу даёт retry, timeout, hooks и HTTPError без зависимостей.
Повторное использование соединений с Agent и Pool из Undici
Для высоконагруженных циклов — парсинга сотен страниц, пакетных API-вызовов, polling сервиса — повторное использование TCP-соединений экономит заметное время. Каждое новое соединение означает новый DNS lookup, TCP-handshake и, для HTTPS, TLS-negotiation.
Поскольку fetch в Node работает на Undici, можно передать свой dispatcher:
import { Agent } from 'undici';
const agent = new Agent({
keepAliveTimeout: 10_000,
keepAliveMaxTimeout: 60_000,
});
const response = await fetch('https://api.example.com/data', {
dispatcher: agent,
});
Ещё больше контроля для конкретного origin:
import { Pool } from 'undici';
const pool = new Pool('https://api.example.com', { connections: 10 });
const response = await fetch('https://api.example.com/data', {
dispatcher: pool,
});
// Когда закончите:
await pool.close();
Бенчмарки в README Undici показывают, что повторное использование соединений и pooling могут сильно поднять throughput — undici - dispatch показал примерно 22 234 req/sec против примерно 5 904 req/sec у undici - fetch в локальном тесте. Реальные цифры будут другими, но направление ясно: если вы делаете много запросов к одному и тому же origin, pooling важен.
Ещё один момент: всегда считывайте или отменяйте response body. Нечитанные тела могут приводить к утечкам ресурсов во внутренних HTTP-механизмах Node.
Потоковая обработка ответов с Node.js Fetch API
Большие загрузки файлов, chunked JSON-потоки, server-sent events, вывод LLM — всё это случаи, когда ожидание полного ответа до начала обработки тратит время и память. Streaming позволяет обрабатывать данные по мере поступления.

В Node 18+ есть совместимый с браузером ReadableStream. Вот как стримить JSON, разделённый переводами строк, и обрабатывать каждую строку по мере её поступления:
const response = await fetch('https://example.com/large-file.ndjson');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (line) {
const item = JSON.parse(line);
console.log('Обработано:', item.id);
}
}
}
Для более простого текстового стриминга, например чтобы направить вывод LLM в stdout:
const response = await fetch('https://example.com/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value, { stream: true }));
}
Streaming — одна из тех областей, где отлично себя показывают и нативный fetch, и Got. У Axios поддержка streaming более ограничена.
Когда fetch() упирается в пределы: структурированный web scraping через API
В какой-то момент fetch уже не является узким местом. Настоящая проблема становится другой: «У меня есть HTML, и что теперь?»

Fetch — это HTTP-клиент: он получает байты, текст, JSON или HTML. Он не понимает, что такое карточка товара, цена, рейтинг или таблица контактов. Для структурированного web scraping обычно получается такой сырой стек:
fetch()для загрузки HTML- Cheerio (или аналог) для выбора элементов через CSS-селекторы
- Собственная логика пагинации
- Рендеринг JavaScript, когда страницы создаются на клиенте
- Прокси / anti-bot / CAPTCHA
- Поддержка селекторов каждый раз, когда меняется верстка сайта
Вот типичный пример fetch + Cheerio — около 15 строк, чтобы вытащить названия товаров:
import * as cheerio from 'cheerio';
const response = await fetch('https://example-store.com/products');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const $ = cheerio.load(html);
const products = $('.product-card')
.map((_, el) => ({
name: $(el).find('.product-title').text().trim(),
price: $(el).find('.price').text().trim(),
url: new URL($(el).find('a').attr('href'), response.url).href,
}))
.get();
console.log(products);
Это работает для стабильных страниц с предсказуемым HTML. Но такая схема быстро становится хрупкой: контент, отрисованный JavaScript, меняющиеся названия классов, антибот-защита и пагинация всё усложняют.
Thunderbit Open API: от сырого HTML к структурированным данным за один вызов
Вот здесь и становится полезен другой тип инструмента. В Thunderbit мы сделали API-слой, который берёт на себя грязную работу — рендеринг JavaScript, защиту от ботов, изменения верстки — чтобы вы могли сосредоточиться на нужных данных.
Distill API (POST /distill): превращает любой URL в чистый Markdown. Полезно для подачи в LLM, построения баз знаний или анализа контента — HTML-парсер не нужен.
Extract API (POST /extract): вы описываете JSON Schema с нужными структурированными данными (название товара, цена, рейтинг), а AI извлекает их. Никаких CSS-селекторов и никакой поломки при изменении верстки.
Вот та же задача по извлечению товаров через Thunderbit Extract API — с вызовом через нативный fetch:
const response = await fetch('https://openapi.thunderbit.com/openapi/v1/extract', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.THUNDERBIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example-store.com/products',
renderMode: 'basic',
schema: {
type: 'object',
properties: {
products: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Название товара' },
price: { type: 'string', description: 'Показанная цена товара' },
rating: { type: 'number', description: 'Средний рейтинг покупателей' },
},
required: ['name', 'price'],
},
},
},
required: ['products'],
},
}),
});
if (!response.ok) throw new Error(`Thunderbit API: ${response.status}`);
const result = await response.json();
console.log(result.data);
Сравнение простое: около 15 строк fetch + Cheerio (и хрупкие селекторы вдобавок) против одного API-вызова, который возвращает чистый JSON. Для batch-задач Thunderbit поддерживает до 50 URL в одном batch extract и до 100 URL в batch distill.
Thunderbit — не замена fetch, fetch — это транспорт. Thunderbit — это слой извлечения, к которому вы переходите, когда сырой HTML-парсинг становится реальной проблемой. Если вам интересна цена, free tier даёт 600 API units для экспериментов, а платные тарифы начинаются от $6 в месяц. Также можно посмотреть расширение Thunderbit для Chrome для no-code извлечения прямо в браузере.
Больше о подходах к структурированному scraping можно прочитать в наших гайдах: лучшие инструменты для извлечения данных, как создать web scraper и извлечение данных с сайта в Excel — там подробно разобраны конкретные сценарии.
Быстрая шпаргалка: Node.js Fetch API
Этот раздел стоит добавить в закладки. Возвращайтесь сюда, когда нужен шаблон для копипаста.
| Паттерн | Фрагмент |
|---|---|
| Базовый GET | const res = await fetch(url); const data = await res.json(); |
| Базовый POST | await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); |
| Проверка HTTP-ошибки | if (!res.ok) throw new Error(\\HTTP ${res.status}\); |
| Таймаут (простой) | await fetch(url, { signal: AbortSignal.timeout(5000) }); |
| Ручная отмена | const c = new AbortController(); setTimeout(() => c.abort(), 5000); await fetch(url, { signal: c.signal }); |
| Статусы для retry | Повторяйте 408, 429, 500, 502, 503, 504. Не делайте blind retry для POST. |
| JSON-wrapper | Используйте fetchJSON(), чтобы проверять ok, парсить content type и выбрасывать HTTPError. |
| Пул соединений | import { Pool } from 'undici'; const pool = new Pool(origin, { connections: 10 }); fetch(url, { dispatcher: pool }); |
| Потоковые чанки | const reader = res.body.getReader(); loop over await reader.read() |
| Структурированное извлечение | Используйте Thunderbit Extract API, когда цель — поля со страницы, а не сырой HTML. |
Заключение и ключевые выводы
Нативный fetch в Node.js готов к продакшену в 2026 году — для новых проектов не нужен node-fetch, и отдельная зависимость Axios по умолчанию тоже не обязательна. Но сам по себе сырой fetch() — это ещё не production-стратегия для HTTP.
Пять вещей, которые большинство туториалов пропускают, и которые мы разобрали здесь:
- Ловушка с ошибками:
fetch()не бросает исключение на 4xx/5xx. Всегда проверяйтеresponse.okили используйте wrapper вродеfetchJSON(). - Таймауты: для простых случаев используйте
AbortSignal.timeout().AbortSignal.timeout()бросаетTimeoutError, а ручнойcontroller.abort()—AbortError. - Логика retry: встроенной нет. Добавьте экспоненциальный backoff для идемпотентных запросов и временных сбоев. Или используйте Ky, где retry уже есть из коробки.
- Повторное использование соединений: для высоконагруженных циклов используйте
AgentилиPoolиз Undici через опциюdispatcher. - Структурированное извлечение: когда вам нужны данные со страниц, а не просто сырой HTML, лучше взять API для извлечения, например Thunderbit, вместо поддержки хрупких CSS-селекторов.
Матрица выбора в одном предложении: используйте нативный fetch для большинства проектов, Axios — для interceptors, Got — для встроенного retry и HTTP/2, Ky — для fetch с лучшими настройками по умолчанию, а API Thunderbit — когда ваши fetch-скрипты для scraping становятся слишком сложными в поддержке.
Попробуйте Thunderbit для извлечения структурированных данных
Попробуйте паттерны из этого руководства. А если хотите увидеть, как Thunderbit справляется со структурированным извлечением, хороший старт — это free tier или обзор на YouTube-канале Thunderbit.
Попробуйте Thunderbit для AI Web Scraping Get Started Free
FAQ
1. Fetch встроен в Node.js или его нужно устанавливать?
Fetch встроен в Node.js начиная с 18-й версии — устанавливать ничего не нужно. Он стал стабильным в Node 21 и полностью поддерживается в Node 22 LTS и Node 24 LTS. Для более старых версий Node можно использовать npm-пакет node-fetch, но новые проекты лучше сразу ориентировать на поддерживаемый LTS-релиз.
2. Бросает ли fetch ошибку на ответы 404 или 500?
Нет. Fetch отклоняет Promise только при сбоях на уровне сети: ошибки DNS, отсутствие соединения, прерванные запросы. HTTP-ответы вроде 404, 403 и 500 успешно завершаются, но с response.ok === false. Нужно явно проверять response.ok или response.status — либо использовать wrapper вроде fetchJSON(), показанный в этом руководстве.
3. Как добавить таймаут к fetch в Node.js?
Самый простой способ — AbortSignal.timeout(ms), доступный в Node 18.11+: await fetch(url, { signal: AbortSignal.timeout(5000) }). Если запрос длится больше 5 секунд, будет выброшен TimeoutError. Для большего контроля можно вручную создать AbortController и вызвать controller.abort() из setTimeout. Для ручного паттерна ловите AbortError, а для AbortSignal.timeout() — TimeoutError.
4. Можно ли использовать fetch для web scraping в Node.js?
Да, но fetch возвращает только сырой HTML. Чтобы извлечь конкретные элементы, понадобится парсер вроде Cheerio, а также собственная логика для пагинации, страниц с JavaScript-рендерингом и anti-bot-защиты. Для структурированного извлечения данных в масштабе — когда вам нужен чистый JSON с названиями товаров, ценами или контактами — рассмотрите Thunderbit Extract API, который использует AI, чтобы возвращать структурированные данные без CSS-селекторов и кода, завязанного на верстку.
5. Стоит ли в 2026 году переходить с Axios на нативный fetch?
Для новых проектов на Node 22+ нативный fetch — очень сильный вариант по умолчанию. Он без зависимостей, работает через Promise и использует тот же API, что и browser fetch. Оставляйте Axios, если вам нужны interceptors запросов/ответов, автоматическое отклонение HTTP-ошибок или обратная совместимость со старыми версиями Node. Оба варианта валидны — выбор зависит от того, какие функции реально использует ваш проект.
Узнать больше


