Node.js Fetch API: Что упускает каждый туториал (гайд 2026)

Последнее обновление: May 14, 2026
Node.js Fetch API: Что упускает каждый туториал (гайд 2026)
Сводка ИИ
Перестаньте полагаться на базовые вызовы fetch в продакшене. Узнайте, как в Node.js в 2026 году обрабатывать таймауты, HTTP-ошибки, retry-логику и пул соединений.

Каждый туториал по fetch в Node.js учит вас писать await fetch(url) — и на этом всё. А потом ваше продакшен-приложение молча проглатывает ошибку 500, один запрос висит 90 секунд без таймаута, и в пятницу вечером вы отлаживаете то, что должно было быть очевидным.

Я уже какое-то время делаю внутренние инструменты и data pipeline в Thunderbit, и могу сказать так: разрыв между «fetch работает в моём туториале» и «fetch работает в продакшене» — это именно то место, где и живёт большая часть боли. Один разработчик на Reddit очень точно сказал: «когда вы идёте в продакшен, понимаете, что вам нужно что-то более надёжное, чем нативный fetch».

Другой признался: «3 года работал веб-разработчиком и только сегодня узнал, что блок catch в fetch API НЕ для HTTP-ошибок». Это руководство покрывает пять вещей, которые большинство туториалов пропускают: ловушку с ошибками, таймауты через AbortController, логику повторных попыток, повторное использование соединений и момент, когда стоит уйти от fetch к извлечению структурированных данных. Если у вас когда-либо fetch-запрос тихо падал в продакшене — это для вас.

nodejs-fetch-error-handling-flowchart.png

Что такое Node.js Fetch API?

Node.js Fetch API — это встроенный, совместимый с браузером способ отправлять HTTP-запросы (GET, POST, PUT, DELETE и т. д.) из Node.js — без установки Axios, node-fetch или любого другого пакета. Если вы уже использовали fetch() в браузере, синтаксис вам знаком. Теперь тот же API работает и на сервере.

Вот короткая история версий:

ЭтапВерсия NodeЧто произошло
Экспериментальный флаг fetchv17.5.0 / v16.15.0fetch добавлен за флагом --experimental-fetch
fetch по умолчанию в глобальной областиv18.0.0Экспериментальный fetch стал доступен глобально, на базе Undici
Стабильный fetchv21.0.0Больше не экспериментальный
База для продакшена в 2026v22 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 сформулировал это так: «зачем импортировать библиотеку, если в языке/фреймворке эта функциональность уже встроена?» Справедливое замечание — но ответ зависит от ваших задач.

http-client-libraries-comparison.png

ФункцияНативный fetchnode-fetch v3axiosgot v15ky v2
Версия Node.js≥18 (рекомендуются 22/24 LTS)≥12.20Широкая≥22≥22
Нужна установкаНетДаДаДаДа
Поддержка ESM + CJSОба варианта (глобально)Только ESM (v3)ОбаТолько ESMТолько ESM
Авто-ошибка на 4xx/5xxНетНетДаДаДа
Встроенный retryНетНетНетДаДа
Request interceptorsНетНетДаДа (hooks)Да (hooks)
Поддержка streamingWeb 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-error-handling-flowchart.png

Именно здесь возникает большинство багов 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

request-retry-pooling-flowchart.png

Без таймаута 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 позволяет обрабатывать данные по мере поступления.

streaming-data-chunking-process.png

В 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, и что теперь?»

web-scraping-vs-ai-extraction-api-comparison.png

Fetch — это HTTP-клиент: он получает байты, текст, JSON или HTML. Он не понимает, что такое карточка товара, цена, рейтинг или таблица контактов. Для структурированного web scraping обычно получается такой сырой стек:

  1. fetch() для загрузки HTML
  2. Cheerio (или аналог) для выбора элементов через CSS-селекторы
  3. Собственная логика пагинации
  4. Рендеринг JavaScript, когда страницы создаются на клиенте
  5. Прокси / anti-bot / CAPTCHA
  6. Поддержка селекторов каждый раз, когда меняется верстка сайта

Вот типичный пример 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

Этот раздел стоит добавить в закладки. Возвращайтесь сюда, когда нужен шаблон для копипаста.

ПаттернФрагмент
Базовый GETconst res = await fetch(url); const data = await res.json();
Базовый POSTawait 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.

Пять вещей, которые большинство туториалов пропускают, и которые мы разобрали здесь:

  1. Ловушка с ошибками: fetch() не бросает исключение на 4xx/5xx. Всегда проверяйте response.ok или используйте wrapper вроде fetchJSON().
  2. Таймауты: для простых случаев используйте AbortSignal.timeout(). AbortSignal.timeout() бросает TimeoutError, а ручной controller.abort()AbortError.
  3. Логика retry: встроенной нет. Добавьте экспоненциальный backoff для идемпотентных запросов и временных сбоев. Или используйте Ky, где retry уже есть из коробки.
  4. Повторное использование соединений: для высоконагруженных циклов используйте Agent или Pool из Undici через опцию dispatcher.
  5. Структурированное извлечение: когда вам нужны данные со страниц, а не просто сырой 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. Оба варианта валидны — выбор зависит от того, какие функции реально использует ваш проект.

Узнать больше

Fawad Khan
Fawad Khan
Фавад зарабатывает на жизнь писательством — и, честно говоря, ему это даже нравится. Он годами разбирался, что делает текст цепляющим, а что заставляет читателя пролистнуть дальше. Спросите его о маркетинге — и он будет говорить часами. Спросите о карбонаре — и он будет говорить еще дольше.
Содержание

Собирай страницу, просто задав вопрос

Скажи, что тебе нужно, простыми словами. А ещё лучше — ничего не говори.

Попробовать Thunderbit бесплатно
Извлекай данные с помощью ИИ
Легко передавай данные в Google Sheets, Airtable или Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week