mrkeyoor.com_
Wed 05 Aug 19:52 UTC
npmUtilsupdated 05 Aug 2026

node-fetch

node-fetch brought the browser's fetch() API to Node.js years before Node had one built in. It wraps Node's http/https modules in a window.fetch-compatible interface: the same Request, Response, and Headers classes, promise-based calls, and body helpers like res.json() and res.text(). It adds server-side conveniences the browser API lacks, such as headers.raw() for reading multiple Set-Cookie values, a redirect limit, a response size limit, and custom http.Agent support. Response bodies are native Node streams. The historical importance is huge; the practical question in 2026 is whether you need it at all, because Node 18 and later ship a native fetch built on undici.

Verdict

A landmark package that native fetch has mostly retired. Keep it in older codebases and pre-18 Node targets; for new projects on modern Node, use the built-in fetch or undici and do not add this dependency.

API stability4/5It tracks the WHATWG fetch spec, so the API surface barely moves; the one big rupture was v3 going ESM-only in 2021, which still splits the user base between v2 and v3.
Docs4/5The README is long and example-driven with a full API reference and upgrade guides, but v2 and v3 docs live in different branches and it is easy to read the wrong one.
Maintenance2/5Last release was 3.3.2 in July 2023, the v4 beta has not landed, and about 252 issues and PRs sit open; the repo saw pushes into May 2026 but shipping has stalled.
Ecosystem4/5Roughly 180M weekly downloads and years of Stack Overflow answers mean help is everywhere, but the ecosystem's center of gravity has moved to native fetch and undici.

Use it if

  • You maintain an existing codebase already built on node-fetch and its FetchError handling, where ripping it out has no payoff
  • You are stuck supporting Node versions older than 18 that have no native fetch, since v3 runs on Node 12.20+ and v2 goes back further
  • You specifically need its extensions over spec fetch: headers.raw() for Set-Cookie arrays, per-request http.Agent injection for proxies and keep-alive tuning, or the highWaterMark stream knob
  • You want response bodies as classic Node streams to pipe directly into fs or transform pipelines without touching web streams
Skip it if

Setup reality

npm install node-fetch pulls three small dependencies (fetch-blob, formdata-polyfill, data-uri-to-buffer). The real setup pain is module format: v3 is ESM-only, so CommonJS projects either pin node-fetch@2, use an awkward dynamic import() wrapper, or migrate their build. TypeScript users on v2 also need the separate @types/node-fetch package, while v3 bundles types. Then there are spec deviations to learn: 3xx-5xx responses do not reject the promise (check res.ok yourself), cookies are not stored, and there is no built-in timeout option since v3, only AbortSignal. If native fetch is also in scope, watch for subtle behavior differences around redirects and agents when switching between the two.

Patterns

GET a JSON endpointget-json

import fetch from 'node-fetch';

const response = await fetch('https://api.github.com/users/github');
const data = await response.json();
console.log(data.login);

res.json() rejects on invalid JSON and the body can only be consumed once; check response.bodyUsed if you see 'body used already' errors.

POST a JSON bodypost-json

import fetch from 'node-fetch';

const response = await fetch('https://httpbin.org/post', {
  method: 'POST',
  body: JSON.stringify({ a: 1 }),
  headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();

Unlike passing URLSearchParams or FormData, a string body sets Content-Type to text/plain unless you set the header yourself.

Use it from CommonJScommonjs-loading

// option 1: stay on the v2 line, which is CommonJS
// npm install node-fetch@2
const fetch = require('node-fetch');

// option 2: dynamic import of v3 from CJS
const fetch3 = (...args) =>
  import('node-fetch').then(({ default: f }) => f(...args));

require('node-fetch') on v3 throws ERR_REQUIRE_ESM. v2 (2.7.0) only gets critical fixes, and its TypeScript types come from @types/node-fetch.

Treat 4xx/5xx as failureshandle-http-errors

import fetch from 'node-fetch';

const response = await fetch('https://httpbin.org/status/500');
if (!response.ok) {
  const body = await response.text();
  throw new Error(`HTTP ${response.status}: ${body}`);
}

Per the fetch spec, 3xx-5xx responses resolve normally; only network and operational failures reject, as FetchError. Forgetting the ok check is the classic bug.

Time out a request with AbortSignaltimeout-with-abort

import fetch, { AbortError } from 'node-fetch';

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
  const res = await fetch('https://slow.example.com', {
    signal: controller.signal
  });
  console.log(res.status);
} catch (err) {
  if (err instanceof AbortError) console.error('timed out');
} finally {
  clearTimeout(timer);
}

The v2-era timeout option is gone in v3; AbortController is the only mechanism. Aborting also destroys an in-flight body stream.

Stream a download to diskstream-to-file

import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import fetch from 'node-fetch';

const response = await fetch('https://example.com/big.zip');
if (!response.ok) throw new Error(`unexpected ${response.status}`);
await pipeline(response.body, createWriteStream('./big.zip'));

response.body is a classic Node readable stream, not a web stream like native fetch returns; pipeline handles error propagation and cleanup.

Keep-alive and per-protocol agentscustom-agent

import http from 'node:http';
import https from 'node:https';
import fetch from 'node-fetch';

const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });

const res = await fetch('https://api.example.com', {
  agent: (url) => (url.protocol === 'http:' ? httpAgent : httpsAgent)
});

The agent option is a node-fetch extension that native fetch does not have; it is also the hook for proxy agents like https-proxy-agent.

Read every Set-Cookie headerread-set-cookie

import fetch from 'node-fetch';

const response = await fetch('https://example.com/login');
const cookies = response.headers.raw()['set-cookie'] ?? [];
for (const c of cookies) console.log(c.split(';')[0]);

headers.get('set-cookie') joins multiple cookies into one comma-separated string, which breaks on cookies containing commas; raw() returns the real array.

Upload multipart form data with a fileupload-form-data

import fetch, { FormData, fileFromSync } from 'node-fetch';

const form = new FormData();
form.set('field', 'value');
form.set('avatar', fileFromSync('./avatar.png', 'image/png'));

const res = await fetch('https://httpbin.org/post', {
  method: 'POST',
  body: form
});

v3 exports spec-compliant FormData plus fileFrom/fileFromSync helpers; the old form-data package from v2 days is no longer the recommended path.

Polyfill global fetch on old Nodeglobal-polyfill

// fetch-polyfill.js
import fetch, { Headers, Request, Response } from 'node-fetch';

if (!globalThis.fetch) {
  globalThis.fetch = fetch;
  globalThis.Headers = Headers;
  globalThis.Request = Request;
  globalThis.Response = Response;
}

Only useful on Node 16 and earlier. On Node 18+ this guard leaves the native fetch in place, so the import becomes dead weight.

Control redirect behaviorlimit-redirects

import fetch from 'node-fetch';

const res = await fetch('https://httpbin.org/redirect/3', {
  redirect: 'follow', // 'manual' | 'error' also supported
  follow: 5 // node-fetch extension: max redirect count
});
console.log(res.redirected, res.url);

follow: 0 with redirect: 'follow' errors on any redirect; redirect: 'manual' hands you the 3xx response with its location header untouched.

Alternatives

PackageRegistryPick it when
undicinpmYou want the engine behind Node's native fetch directly, with interceptors, connection pooling, and the fastest request path.
gotnpmYou want retries, hooks, pagination, and HTTP/2 handled for you in a Node-only ESM client.
axiosnpmYou need one API across browser and Node with interceptors and wide team familiarity, and CommonJS support matters.