mrkeyoor.com_
Sat 19 Sept 23:46 UTC
npmUtilsupdated 19 Sept 2026

node-fetch review

node-fetch 3.3.2 provides the browser-shaped fetch API on top of Node HTTP and exposes response bodies as Node Readable streams. It has server-specific controls for maximum response size, redirect count, custom agents, raw response headers, compression, and stream buffering. Node 18 and newer already include fetch through Undici, so the package now mainly serves older runtimes and existing applications that depend on node-fetch extensions or error classes. Version 3.3.2 is still the current stable release. Our browser bundle failed because this implementation is built for Node.

176.4Mdownloads / wk
Verdict

node-fetch 3.3.2 installed in 0.9 seconds but left 10 MB across 9 packages and 1 deprecation warning on our Node 22 box, where fetch already exists. Keep it when old runtime support or node-fetch extensions are contractual; do not add it by habit to a current Node service.

We installed it

Lab card: what happened when we installed node-fetchScreenshot of node-fetch documentation
Install✓ · 0.9s9 packages on disk · 10 MB · 1 deprecation warning
ImportESM import works · require() works · ESM package
Browsern/acould not be bundled for the browser (Node-only code, most likely)
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does node-fetch install cleanly?

Yes. In a fresh container with an empty cache, npm install node-fetch finished in 0.9s, leaving 9 packages and 10 MB on disk. npm audit reported no known vulnerabilities. The install printed 1 deprecation warning.

Can node-fetch run in a browser?

Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.

Does node-fetch work with both ESM and CommonJS?

Yes. Both import 'node-fetch' and require('node-fetch') worked in Node 22 in our run. The package is published as ESM.

Does node-fetch include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

node-fetch or undici: which should you use?

undici: Use it for Node's fetch engine plus explicit pools, dispatchers, interceptors, and lower-level control. node-fetch 3.3.2 installed in 0.9 seconds but left 10 MB across 9 packages and 1 deprecation warning on our Node 22 box, where fetch already exists.

When should you not use node-fetch?

The application starts on Node 18 or newer and needs ordinary fetch; the runtime already provides it

API stability3/5The everyday fetch(url, options) shape is familiar, but version 3's documented ESM-only move split migration paths between native ESM, dynamic import, and the CommonJS v2 line. Node Readable bodies and extensions such as headers.raw(), size, follow, agent, and highWaterMark also bind callers to this implementation. With 3.3.2 still current, there is limited recent release evidence for how those extensions will evolve.
Docs5/5The README documents module loading, JSON and form bodies, Node streams, HTTP error handling, abort signals, custom agents, redirect behavior, compression, response limits, raw headers, and clone buffering. It directly explains the 16 KB default highWaterMark and why serial consumption of large cloned responses can hang. The branch matters, though: v2 CommonJS users should read v2 material rather than copying v3 examples.
Maintenance2/5npm still identifies 3.3.2, published in July 2023, as latest. GitHub showed a push on May 12, 2026 and the project is unarchived, but its open count was 251 issues and pull requests. Repository activity prevents an abandoned label; the absence of a newer stable package and modern Node's shift to built-in fetch make prompt feature or bug-fix releases a poor planning assumption.
Ecosystem5/5npm counted 191,340,897 downloads between August 19 and August 25, 2026, and GitHub showed 8,856 stars. Years of transitive use, tutorials, and adapters explain that huge footprint. The number should not be read as a greenfield recommendation: Node 18+ global fetch and direct Undici APIs now receive the runtime's main attention, while much node-fetch demand comes from older dependency trees.

Discussed on

  1. hnNode.js Security Fix Silently Broke node-fetch, which broke other tools4 points
  2. hnNode-Fetch on latest Node Update broken4 points

Use it if

  • An established service depends on FetchError, headers.raw(), Node Readable bodies, size limits, or custom agent behavior
  • The supported runtime predates Node's global fetch
  • Redirect limits and maximum response sizes already form part of the application's network policy
  • Replacing a working client would risk behavior changes without removing meaningful operational cost
Skip it if

Setup reality

We installed node-fetch 3.3.2 in our clean Node 22 sandbox in 0.9 seconds. npm left 9 packages using 10 MB and printed 1 deprecation warning. npm audit found 0 known vulnerabilities. The package has 3 direct dependencies, 0 peers, a 168 KB unpacked size, bundled TypeScript declarations, and an MIT license. Its engines range covers Node 12.20, 14.13.1, or 16 and newer, although current Node already supplies fetch.

node-fetch 3 is an ESM package without an exports map. Both require() and ESM import happened to work in our generic Node 22 probe, but the project's documented contract says direct require is unsupported. CommonJS should use an async import wrapper or remain on the separately maintained v2 line. The browser-targeted esbuild run failed. Keep node-fetch out of shared client modules and do not treat loader interop on 1 test box as a packaging promise.

A 404 or 500 resolves to a Response, just like browser fetch. Check ok or status before parsing a success body. There is no cookie jar, automatic retry, or timeout option. Cancellation uses AbortSignal, so clear any manual timer in finally. For large downloads, await stream.pipeline(response.body, destination); a loose pipe can lose errors or leave a partial file that looks complete. Limit response size when the remote body is untrusted.

Agents control proxies, TLS, and connection reuse. Redirects may change protocol, so an agent function sometimes has to select HTTP or HTTPS per URL. Manual redirects differ from browser opaque redirects. Cloning a large body can stall if its 2 branches are consumed serially because Node's stream buffer is smaller; read clones in parallel or set highWaterMark with a measured reason. On Node 22, test the built-in fetch first before preserving these package-specific edges.

Patterns

Fetch JSON after checking status get-json

import fetch from 'node-fetch';
const response = await fetch('https://api.example.com/item/42');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const item = await response.json();

node-fetch rejects on network failure, but 4xx and 5xx responses still resolve normally.

Send an explicit JSON body post-json

const response = await fetch(url, {
  method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ enabled: true })
});

A plain object is not serialized automatically; set content type and call JSON.stringify yourself.

Load version 3 from CommonJS load-from-commonjs

const fetch = (...args) => import('node-fetch').then(({ default: fn }) => fn(...args));
const response = await fetch('https://example.com');

The v3 README specifies ESM only, so CommonJS uses asynchronous import or stays on the v2 release line.

Preserve a bounded error response capture-http-error

const response = await fetch(url, { size: 1_000_000 });
if (!response.ok) {
  const body = (await response.text()).slice(0, 4096);
  throw new Error(`HTTP ${response.status}: ${body}`);
}

The 1000000-byte body cap applies during consumption; the 4096-character slice keeps logs bounded.

Abort a slow request abort-timeout

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try { return await fetch(url, { signal: controller.signal }); }
finally { clearTimeout(timer); }

Always clear the 5000 ms timer; abort errors and underlying network timeouts can have different causes.

Write a body with pipeline stream-download

import { pipeline } from 'node:stream/promises';
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
await pipeline(response.body, createWriteStream('./download.bin'));

pipeline forwards stream errors and waits for completion, avoiding a silently partial download.

Select an agent after redirects choose-agent

const response = await fetch(url, { agent: parsed =>
  parsed.protocol === 'http:' ? httpAgent : httpsAgent
});

A redirect can cross protocols, so the callback runs with each URL rather than assuming HTTPS forever.

Read every Set-Cookie value read-cookies

const response = await fetch(loginUrl);
const setCookies = response.headers.raw()['set-cookie'] ?? [];

headers.raw() is a node-fetch extension; the package does not store or resend those cookies for you.

Upload a file with FormData upload-form

import fetch, { FormData, fileFrom } from 'node-fetch';
const form = new FormData();
form.set('document', await fileFrom('./report.pdf'));
await fetch(url, { method: 'POST', body: form });

Let FormData set the multipart boundary; manually setting content-type would omit the generated boundary value.

Consume cloned bodies in parallel consume-clones

const response = await fetch(url);
const copy = response.clone();
const [json, text] = await Promise.all([response.json(), copy.text()]);

Parallel consumption prevents 1 branch from filling the Node stream buffer while the other waits.

Alternatives

PackageRegistryPick it when
undicinpmUse it for Node's fetch engine plus explicit pools, dispatchers, interceptors, and lower-level control
gotnpmUse it when retries, hooks, pagination, and Node-oriented request features belong in the client
axiosnpmUse it for browser and Node calls with interceptors and CommonJS-friendly packaging

More utils guides

lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.