Общение с HTTP-конечной точкой раньше означало, что нужно тянуться за сторонним пакетом. На сервере это больше не так. Начиная с Node.js 18, глобальная функция fetch поставляется в среде выполнения, поэтому вы можете выполнять HTTP-запросы в Node.js с помощью Fetch API, используя ровно тот же promise-интерфейс, который вы уже знаете по браузеру, без установки node-fetch и без флага, который надо переключать.

Это руководство практический тур по fetch на сервере: GET и POST, строки запроса, отправка и чтение JSON, правило обработки ошибок, на котором спотыкаются почти все, таймауты с AbortController, параллельный запуск запросов и где Axios всё ещё заслуживает своё место. Оно закрывается одним сценарием, где голого запроса недостаточно, скрапингом реального сайта, и тем, как вместо этого получать отрисованный HTML через API.

Почему fetch и что изменилось в Node

Fetch API основан на промисах, поэтому он чисто компонуется с async/await и избегает вложенности колбэков старого модуля http. Он говорит на всех методах, которые вам нужны (GET, POST, PUT, PATCH, DELETE), даёт вам настоящий объект Headers и обрабатывает JSON одним вызовом метода. Интерфейс идентичен в браузере и на сервере, а это означает, что код и мысленные модели переносятся в обе стороны.

Практический заголовок это граница версий. На Node.js 18 и новее fetch глобальна, ровно как setTimeout. Вы её не импортируете и ничего не устанавливаете. Подтвердите свою среду выполнения перед написанием кода:

bash
node -v
# v18.x or higher means global fetch is available
Нужен ли вам ещё node-fetch?

Только на Node 16 или старше, где вы устанавливали бы node-fetch и импортировали его. На Node 18+ этот пакет избыточен, и его добавление лишь затеняет более быструю встроенную реализацию. Если вы на текущем LTS-релизе, удалите зависимость и используйте глобальную функцию.

Выполнение GET-запросов с Fetch API

GET-запрос это один вызов. fetch() возвращает промис, который разрешается в объект Response, как только приходят заголовки. Тело читается отдельно, с response.json() для JSON или response.text() для простого текста, и каждый из них возвращает собственный промис.

Деталь, которая важнее всего: всегда проверяйте response.ok перед чтением тела. 404 или 500 это всё ещё успешный HTTP-обмен с точки зрения fetch, поэтому промис разрешается нормально. response.ok равно true только для кодов состояния в диапазоне от 200 до 299.

javascript
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}`);
  }

  const data = await response.json();
  return data;
}

getPost(1).then(console.log).catch(console.error);

Если конечная точка возвращает текст или HTML, а не JSON, поменяйте читатель тела. Остальная часть потока не меняется.

javascript
const response = await fetch("https://example.com/");
const html = await response.text();
console.log(html.slice(0, 200));

Тело можно прочитать ровно один раз. Вызов response.json() после response.text() на том же ответе выбрасывает исключение, потому что поток уже потреблён. Выбирайте один читатель на ответ.

Чтение и установка заголовков ответа

Объект Response предоставляет свойство headers, которое ведёт себя как Map. Читайте один заголовок с помощью get() или итерируйте весь набор. Имена заголовков нечувствительны к регистру, поэтому "content-type" и "Content-Type" разрешаются в одно и то же значение.

javascript
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");

console.log(response.headers.get("content-type"));

for (const [key, value] of response.headers) {
  console.log(`${key}: ${value}`);
}

Одно преимущество работы на сервере: ограничения CORS, которые скрывают большинство заголовков в браузере, в Node не применяются. Вы получаете полный доступ к каждому заголовку ответа, который отправляет сервер.

Добавление строк запроса с URLSearchParams

Вы могли бы склеивать параметры запроса вручную, но это означает самостоятельное экранирование пробелов, амперсандов и других зарезервированных символов, и в этом легко ошибиться. URLSearchParams кодирует значения правильно и читается чисто.

javascript
const params = new URLSearchParams({
  userId: 1,
  _limit: 5,
});

const url = `https://jsonplaceholder.typicode.com/posts?${params}`;
const response = await fetch(url);
const posts = await response.json();

console.log(`Got ${posts.length} posts`);

Интерполяция params в шаблонный литерал вызывает её toString(), который производит правильно закодированную строку запроса. Добавление или удаление фильтра позже это однострочное изменение объекта, а не упражнение в хирургии строк.

Отправка POST-запросов и тел JSON

Чтобы отправить данные, передайте второй аргумент: объект опций с method, headers и body. Тело должно быть строкой, поэтому полезные нагрузки JSON проходят через JSON.stringify(), и вы устанавливаете Content-Type: application/json, чтобы сервер знал, как разбирать то, что он получает.

javascript
async function createPost(payload) {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });

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

  return response.json();
}

const created = await createPost({
  title: "Fetch on the server",
  body: "Built into Node 18+",
  userId: 1,
});

console.log(created);

Та же форма покрывает PUT и PATCH для обновлений и DELETE для удалений: меняйте method и, где уместно, body. Для отправки форм, а не JSON, передайте экземпляр URLSearchParams или FormData в качестве тела и опустите явный Content-Type; fetch устанавливает правильный заголовок для них автоматически.

Обработка ошибок: правило, которое ловит каждого

Вот единственная самая важная вещь, которую нужно усвоить про fetch. Он отклоняется только на сбоях сетевого уровня: разрешение DNS не удалось, соединение оборвалось, запрос был прерван. Любой HTTP-ответ от сервера, включая 404 и 500, считается разрешённым промисом. Голый try/catch вокруг fetch с радостью проглотит 500 и пойдёт дальше, как будто всё в порядке.

Надёжный паттерн совмещает обе проверки: try/catch для сетевого слоя и явный тест response.ok для HTTP-слоя.

javascript
async function getJson(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      // HTTP error: server replied, but with a 4xx or 5xx
      throw new Error(`HTTP ${response.status} for ${url}`);
    }

    return await response.json();
  } catch (error) {
    // Network failure, abort, or the thrown HTTP error above
    console.error("Request failed:", error.message);
    throw error;
  }
}

Выбрасывание исключения на !response.ok направляет HTTP-ошибки в тот же catch, что и сетевые ошибки, поэтому один блок обрабатывает оба класса сбоев. Без этой явной проверки 500 проскальзывает прямо в response.json(), который затем выбрасывает запутывающую ошибку разбора на HTML-странице ошибки вместо реального статуса.

Таймауты и отмена с AbortController

У fetch нет встроенного таймаута. Оставленный сам по себе, запрос может висеть столько, сколько соединение остаётся открытым, что неприемлемо для всего, что обращено к пользователю. Стандартное решение это AbortController: создайте его, передайте его signal в fetch и вызовите abort(), когда срабатывает таймер.

javascript
async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      throw new Error(`Request to ${url} timed out after ${ms}ms`);
    }
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

Прерванный запрос отклоняется с AbortError, поэтому catch и инспектирует error.name, чтобы дать ясное сообщение о таймауте. Очистка таймера в finally не даёт успешному раннему ответу позже запустить случайный abort. Тот же signal может подключить fetch к управляемой пользователем кнопке отмены, а не только к таймеру. На свежих версиях Node AbortSignal.timeout(ms) это сокращение для ровно этого паттерна с таймером.

Параллельный запуск запросов с Promise.all

Когда у вас есть несколько независимых запросов, ожидание их одного за другим тратит время: каждый ждёт завершения предыдущего. Promise.all запускает их все сразу и разрешается, когда приземляется последний, поэтому общее время это примерно самый медленный запрос, а не сумма всех.

javascript
const ids = [1, 2, 3, 4, 5];

const posts = await Promise.all(
  ids.map((id) =>
    getPost(id).catch((error) => ({ id, error: error.message })),
  ),
);

console.log(posts);

Одна оговорка: Promise.all отклоняется, как только отклоняется любой один промис, отбрасывая остальные. Встроенный .catch() выше превращает сбой отдельного запроса в значение, поэтому один плохой запрос не топит всю партию. Если вы предпочитаете инспектировать каждый исход явно, Promise.allSettled возвращает запись статуса и значения для каждого. И не выпаливайте тысячи запросов сразу: именно так вы получаете лимит частоты или исчерпываете сокеты. Для больших задач ограничивайте параллелизм, обрабатывая список фрагментами фиксированного размера.

Fetch API против Axios: за чем тянуться

Axios предшествует серверному fetch и всё ещё имеет преданную аудиторию. Честное сравнение короткое:

  • Тянитесь за fetch, когда вам нужны нулевые зависимости и стандартный API. Он встроен, он такой же в браузере, и для большинства работы с GET и POST это всё, что вам нужно.
  • Тянитесь за Axios, когда вам нужны удобства, которые он несёт в комплекте: автоматический разбор JSON в обоих направлениях, перехватчики запросов и ответов, встроенная настройка таймаута, автоматическое отклонение на статусе, отличном от 2xx, и прогресс загрузки или выгрузки. На большой кодовой базе эти возможности экономят реальный шаблонный код.

Разрыв меньше, чем раньше. Большинство приятностей Axios, таймауты, проверки статуса, обработка JSON, это несколько строк вспомогательного кода поверх fetch, как обёртки в этой статье. Если вы начинаете с нуля на Node 18+ и вам не нужны перехватчики, встроенный вариант это более стройный выбор. Если проект уже стандартизирован на Axios, нет срочности его вырывать.

Где голый fetch упирается в стену: скрапинг реальных сайтов

Всё вышеперечисленное прекрасно работает против API и кооперативных серверов. Однако направьте fetch на современный коммерческий сайт, чтобы вытянуть его HTML, и вы быстро упрётесь в два жёстких ограничения.

Во-первых, fetch извлекает только исходный HTML, который отправляет сервер. Он не запускает браузер, поэтому никогда не исполняет JavaScript страницы. На сайте, который отрисовывает свой контент на стороне клиента, разметка, которую вы получаете обратно, это почти пустая оболочка, а нужные вам данные заполняются позже скриптами, которые так и не запускаются. Во-вторых, сайты, которым важен скрапинг, снимают отпечаток с входящего трафика. Запрос с IP дата-центра с дефолтным user agent Node и без браузероподобных заголовков получает вызов или блокировку, прежде чем вернёт что-либо полезное.

Вы можете атаковать оба сами, с headless-браузером для отрисовки страницы и пулом вращающихся резидентных прокси, чтобы выглядеть как настоящие посетители, но поддержание этого стека в рабочем состоянии и есть большая часть работы. Более чистый путь это отправить ваш вызов fetch к API, который делает отрисовку и ротацию IP на стороне сервера и отдаёт вам готовый HTML.

Crawlbase Crawling API

Скрапинг реального сайта требует отрисованной страницы за доверенным IP, в одном запросе. Crawling API принимает ваш токен и целевой URL, прогоняет страницу в настоящем браузере, ротирует резидентные IP за вас и возвращает готовый HTML, так что вы продолжаете использовать обычный fetch и избегаете запуска headless-флота и пула прокси. Начните на бесплатном тарифе.

Вызов это тот же fetch, который вы уже знаете. Вы строите URL конечной точки с вашим токеном и URL-кодированной целью, отправляете GET и считываете отрисованный HTML из ответа. Оттуда парсер вроде cheerio превращает этот HTML в структурированные данные.

javascript
import * as cheerio from "cheerio";

const TOKEN = "YOUR_CRAWLBASE_TOKEN";

async function scrape(targetUrl) {
  const params = new URLSearchParams({
    token: TOKEN,
    url: targetUrl,
  });

  const response = await fetch(`https://api.crawlbase.com/?${params}`);
  if (!response.ok) {
    throw new Error(`Crawl failed: HTTP ${response.status}`);
  }

  const html = await response.text();
  const $ = cheerio.load(html);

  return {
    title: $("title").text().trim(),
    headings: $("h2").map((_, el) => $(el).text().trim()).get(),
  };
}

scrape("https://www.example.com/").then(console.log);

Для страниц, которые отрисовывают контент с помощью JavaScript, добавьте эквивалент "&javascript=true", передав токен с JS-отрисовкой, который прогоняет цель в настоящем браузере перед возвратом HTML. Поскольку запрос это просто fetch, каждый приём из этой статьи всё ещё применим: оберните его в помощник с таймаутом, прогоните список URL через Promise.all с разумным ограничением параллелизма и проверяйте response.ok на каждом вызове. Для более полного разбора скрапинга на Node смотрите материал как построить веб-скрапер на Node.js, а конкретно по проблеме отрисовки как обходить JavaScript-сайты.

Итоги

Ключевые выводы

  • fetch встроена в Node 18+. Никакого node-fetch, никакого флага, никакого импорта. Это глобальная функция, такая же, как в браузере.
  • Всегда проверяйте response.ok. fetch отклоняется только на сетевых ошибках, поэтому 404 или 500 разрешается нормально и проскальзывает мимо голого try/catch.
  • POST это второй аргумент. Установите method, заголовок Content-Type и пропущенное через JSON.stringify body; стройте строки запроса с URLSearchParams.
  • Добавляйте таймауты с AbortController. Встроенного таймаута нет; передайте signal и прерывайте по таймеру или действию отмены.
  • Распараллеливайте с Promise.all. Запускайте независимые запросы вместе, защищайте каждый с помощью .catch() и ограничивайте параллелизм для больших партий.
  • Голый fetch не может скрапить современные сайты. Он не будет отрисовывать JavaScript и получает блокировку; маршрутизируйте тот же fetch через Crawling API ради отрисованного HTML, затем парсите с cheerio.

Часто задаваемые вопросы

Нужен ли мне ещё node-fetch в Node.js?

Не на Node.js 18 или новее. fetch там глобальна, поэтому вы можете вызывать её напрямую без установки и без импорта. node-fetch нужен вам только на Node 16 или старше. Если вы на текущем LTS-релизе, удаление зависимости безопасно и позволяет использовать более быструю встроенную реализацию.

Почему fetch не выбрасывает исключение на 404 или 500?

Потому что fetch трактует любой завершённый HTTP-обмен как успех, даже когда код состояния это ошибка. Промис отклоняется только на сбоях сетевого уровня вроде оборванного соединения, сбоя DNS или прерывания. Чтобы ловить HTTP-ошибки, вы должны сами проверять response.ok (true только для 200 до 299) и выбрасывать исключение, когда он false, а затем дать вашему catch его обработать.

Как отправить JSON в POST-запросе с fetch?

Передайте объект опций вторым аргументом с method: "POST", объектом headers, который устанавливает "Content-Type": "application/json", и body, произведённым JSON.stringify(). Тело должно быть строкой, поэтому вы сначала строкуете объект. Прочитайте ответ сервера обратно с await response.json() после подтверждения response.ok.

Как добавить таймаут к запросу fetch?

Используйте AbortController. Создайте его, передайте его signal в опции fetch и вызовите controller.abort() из setTimeout. Прерванный запрос отклоняется с AbortError, поэтому проверяйте error.name в вашем catch, чтобы чисто сообщить о таймауте. На свежих версиях Node AbortSignal.timeout(ms) оборачивает этот паттерн в один вызов.

Использовать fetch или Axios в Node?

Используйте fetch для запросов без зависимостей, основанных на стандартах; он покрывает большую часть работы с GET и POST из коробки на Node 18+. Выбирайте Axios, когда вам нужны его встроенные удобства, такие как перехватчики, автоматическая обработка JSON, настроенные таймауты и автоматическое отклонение на ответах, отличных от 2xx. Для свежего проекта, которому это не нужно, встроенный вариант это более стройный выбор; у существующей кодовой базы на Axios нет срочной причины переключаться.

Можно ли использовать fetch для скрапинга сайта?

Вы можете получить сырой HTML страницы, но у голого fetch два ограничения на реальных сайтах: он не запускает JavaScript, поэтому отрисованный на клиенте контент возвращается пустым, а запросы с IP дата-центров получают вызов или блокировку. Решение это отправить ваш вызов fetch к Crawling API, который отрисовывает страницу в настоящем браузере за вращающимися резидентными IP и возвращает готовый HTML, который вы можете парсить с cheerio.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга