mrkeyoor.com_
Sun 20 Sept 04:57 UTC
npmUtilsupdated 20 Sept 2026

cross-fetch review

`cross-fetch` gives one Fetch-shaped import to code that runs in browsers, workers, React Native, and Node. It selects `whatwg-fetch` in browser-like builds and `node-fetch` 2 in Node, with separate ponyfill and global-polyfill entry points. Version 4.1.0 adds stated Node 22 support, moves its Node dependency to node-fetch 2.7.0, and updates whatwg-fetch to 3.6.20. Our package inspection found a CommonJS distribution with no exports map; both `require()` and ESM `import` worked, and TypeScript declarations were included. The main reason to keep it is shared code that must cover older runtimes. Current Node and evergreen browsers already provide `fetch`.

27.3Mdownloads / wk
Verdict

Our cross-fetch 4.1.0 install completed in 0.6 seconds, occupied 1 MB across 6 packages, and added 3.7 KB gzipped to a full browser import with 0 audit findings. Keep it for genuinely shared code or old Node support; projects whose targets already expose fetch should remove the dependency.

We installed it

Lab card: what happened when we installed cross-fetchScreenshot of cross-fetch documentation
Install✓ · 0.6s6 packages on disk · 1 MB
ImportESM import works · require() works · CommonJS package
Browser3.7 KBgzipped (10.8 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does cross-fetch install cleanly?

Yes. In a fresh container with an empty cache, npm install cross-fetch finished in 0.6s, leaving 6 packages and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does cross-fetch add to a browser bundle?

3.7 KB gzipped (10.8 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does cross-fetch work with both ESM and CommonJS?

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

Does cross-fetch include TypeScript types?

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

cross-fetch or node-fetch: which should you use?

node-fetch: Use it directly when only Node needs a Fetch implementation and browser switching adds no value. Our cross-fetch 4.1.0 install completed in 0.6 seconds, occupied 1 MB across 6 packages, and added 3.7 KB gzipped to a full browser import with 0 audit findings.

When should you not use cross-fetch?

Every supported runtime already implements fetch. On current Node and evergreen browsers, the native API removes 6 installed packages and an extra compatibility layer.

API stability4/5The project stays on the familiar Fetch contract and has kept separate ponyfill and polyfill imports across major releases. Version 4.1.0 changes the implementations underneath by upgrading node-fetch and whatwg-fetch, yet ordinary `fetch(url, init)` code remains intact. Stability is slightly lower than native fetch because runtime selection depends on legacy package fields, and Node receives node-fetch 2 stream behavior rather than the web streams exposed by newer native implementations.
Docs3/5The README clearly shows installation, CommonJS and ESM imports, the polyfill entry, status checking, and the platform-switching design. It also warns that global polyfills carry risk. Its API link points to github.github.io/fetch, which currently returns 404, and it does not document Node stream differences, absent cookie persistence, relative URL behavior, timeout construction, or TypeScript DOM library requirements. Readers need Fetch and node-fetch documentation to fill those gaps.
Maintenance3/5The unarchived repository reports 27 open issues and pull requests, 1,695 stars, and a last push on 2025-04-15. Version 4.1.0 shipped in December 2024 with Node 22 support plus node-fetch 2.7.0 and whatwg-fetch 3.6.20 updates. The package is stable enough that constant releases are unnecessary, but more than a year without a push leaves browser and Node compatibility fixes dependent on older underlying branches.
Ecosystem5/5The npm downloads endpoint counted 37,930,698 downloads for the latest completed week. CommonJS and ESM consumers both loaded the package in our Node 22 test, bundled declarations cover TypeScript, and browser, worker, React Native, and Node targets are all part of the stated contract. Much of that demand is transitive compatibility work. New applications have a strong built-in alternative now that current browsers and maintained Node releases ship fetch directly.

Use it if

  • A published library needs one local Fetch import that works across Node, browsers, workers, and React Native without assuming a global exists.
  • An older Node target still lacks built-in fetch, while browser bundles should use the browser implementation selected by the package field.
  • You want to choose between a ponyfill import and a side-effect polyfill rather than patching the global unconditionally.
  • CommonJS consumers remain in the support matrix and must call `require('cross-fetch')` without an ESM-only migration.
Skip it if

Setup reality

We installed cross-fetch 4.1.0 in a fresh Node 22 Bookworm container. npm finished in 0.6 seconds and left 6 packages using 1 MB on disk. npm audit reported 0 vulnerabilities at every severity. The package itself declares 1 direct dependency and 0 peers and is 140 KB unpacked. It includes TypeScript declarations. Our module checks found a CommonJS package with no exports map, while both require() and ESM import succeeded.

No token, config file, or client object is involved. Import cross-fetch for a local ponyfill, or load cross-fetch/polyfill before dependent modules if they read globalThis.fetch. The polyfill route mutates a shared runtime, which makes load order part of correctness. Library authors should usually prefer the local import so an application keeps control of its global APIs.

A full browser import measured 10.8 KB minified and 3.7 KB gzipped in our esbuild check. Browser builds are redirected to whatwg-fetch; Node receives node-fetch 2.7.0. Those implementations share the Fetch surface but not every runtime detail. In particular, Node response bodies are Node streams, relative URLs lack a browser base URL, and cookie storage is not supplied. React Native and service workers bring their own networking constraints.

Fetch resolves on HTTP 404 or 500. Check response.ok or status before treating the body as success data. It does not encode a JavaScript object as JSON, attach a content type, retry, or impose a deadline. Use an AbortController for cancellation and clear its timer in finally. Version 4.1.0 supports Node 22 according to its release, but the package has no engines declaration to make npm enforce that statement.

Patterns

Fetch JSON without changing globals fetch-json

import fetch from 'cross-fetch';

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

A 4xx or 5xx response fulfills the promise. Test `ok` or `status` before accepting the JSON as a success payload.

Install the global polyfill polyfill-global

import 'cross-fetch/polyfill';

const response = await fetch('https://api.example.com/health');

Import this side-effect entry before code that reads global `fetch`. A local ponyfill is safer for reusable packages.

Load it from CommonJS require-commonjs

const fetch = require('cross-fetch');
const { Headers, Request, Response } = require('cross-fetch');

const response = await fetch(url);

Version 4.1.0 is a CommonJS package without an exports map, and our Node 22 `require()` check succeeded.

Post a JSON body post-json

const response = await fetch(url, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ sku: 'A-42', quantity: 2 }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);

Fetch does not stringify objects or choose a JSON content type. Supply both pieces explicitly.

Cancel after five seconds abort-request

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

The controller pattern covers targets that do not provide `AbortSignal.timeout()`. Always clear the timer after completion.

Construct portable headers set-request-headers

import fetch, { Headers } from 'cross-fetch';

const headers = new Headers({
  accept: 'application/json',
  authorization: `Bearer ${token}`,
});
const response = await fetch(url, { headers });

Import `Headers` from the package when some supported runtimes may not define the constructor globally.

Pipe a Node download stream-file-node

import fetch from 'cross-fetch';
import { createWriteStream } from 'node:fs';

const response = await fetch(fileUrl);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
response.body.pipe(createWriteStream('archive.zip'));

This snippet is Node-specific because the node-fetch 2 path exposes a Node readable. Browser response bodies use web streams.

Preserve an HTTP error body capture-error-body

const response = await fetch(url);
if (!response.ok) {
  const text = await response.text();
  throw new Error(`HTTP ${response.status}: ${text.slice(0, 300)}`);
}
return response.json();

HTTP failures resolve normally, while connection errors and explicit aborts reject. Handle the two paths separately.

Send multipart data from Node upload-form-node

const FormData = require('form-data');
const fs = require('node:fs');
const fetch = require('cross-fetch');

const form = new FormData();
form.append('file', fs.createReadStream('invoice.pdf'));
await fetch(url, { method: 'POST', body: form, headers: form.getHeaders() });

On the node-fetch 2 path, forward `form.getHeaders()` so the declared multipart boundary matches the encoded body.

Read one response two ways clone-response

const response = await fetch(url);
const backup = response.clone();

try {
  return await response.json();
} catch {
  return { raw: await backup.text() };
}

A response body is consumable once. Clone it before the first read when a fallback parser needs the same bytes.

Include Fetch types in Node TypeScript configure-typescript

{
  "compilerOptions": {
    "lib": ["ES2022", "DOM"],
    "types": ["node"]
  }
}

The bundled declarations refer to web names such as `Request` and `Response`; a Node-only tsconfig may still need the DOM library.

Remove the ponyfill on current Node migrate-native-fetch

// Delete: import fetch from 'cross-fetch';
const response = await fetch(url);

// For old Node stream consumers:
// Readable.fromWeb(response.body).pipe(destination);

Audit `response.body.pipe()` and node-fetch-specific error checks first. Native Node fetch uses web stream semantics.

Alternatives

PackageRegistryPick it when
node-fetchnpmUse it directly when only Node needs a Fetch implementation and browser switching adds no value.
undicinpmUse it for Node-focused HTTP with the implementation behind modern Node's native fetch.
kynpmUse it when Fetch plus retries, hooks, timeout controls, and JSON helpers matches the application better.
axiosnpmUse it when interceptors, progress events, adapters, and automatic HTTP status rejection are required.

More utils guides

lru-cache · ajv · type-fest · 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.