mrkeyoor.com_
Thu 06 Aug 02:46 UTC
npmUtilsupdated 06 Aug 2026

cross-fetch

cross-fetch is a switchboard: import it and you get a fetch function that works the same whether the code runs in Node, a browser, React Native, or a worker. It does not implement anything itself. Package.json fields point each environment at a different file, so Node resolves to a wrapper around node-fetch 2.x, browsers resolve to a bundle of GitHub's whatwg-fetch polyfill, and React Native gets its own entry. You can use it as a ponyfill (import fetch from 'cross-fetch' and nothing touches globals) or as a polyfill (import 'cross-fetch/polyfill' to assign global.fetch when it is missing). It became ubiquitous in the years when Node had no fetch at all, which is why it still shows up in tens of millions of installs a week, mostly through SDKs and GraphQL clients rather than direct use.

Verdict

Reasonable only if you still support Node 16 or older, or ship a library that must run identically on React Native. On Node 18+ it is a legacy dependency that routes your requests through a 2023 build of node-fetch instead of the runtime's own client, and removing it is usually a delete-the-import change.

API stability5/5The surface is the WHATWG fetch API plus two import paths, unchanged across major versions; upgrading from 3.x to 4.x mostly meant a newer node-fetch underneath
Docs3/5The README explains ponyfill versus polyfill clearly and links to the fetch spec docs, but there is no reference of its own: streaming limits, the DOM lib type requirement, and the fact that it never uses native fetch on Node are all undocumented
Maintenance2/5Last release 4.1.0 in December 2024, last repo push April 2025, 24 open issues (27 counting PRs), and the Node dependency is pinned to the node-fetch 2.x line whose last release was August 2023
Ecosystem4/5About 35.9M weekly downloads and 1.7k stars, almost all of it transitive through SDKs and GraphQL clients written before Node had fetch; there are no plugins and no ecosystem around it, just a lot of dependents

Use it if

  • You maintain a library that must run unchanged on Node, browsers, and React Native, and you do not want to write the environment detection yourself
  • You support Node versions older than 18, where there is no global fetch and a shim is the only option
  • You need the ponyfill shape specifically: a fetch you import, with no global mutation, so consumers who already polyfilled are unaffected
  • You are patching an existing codebase whose tests or SSR path breaks on 'fetch is not defined' and you want the one-line fix that works everywhere
Skip it if

Setup reality

npm install cross-fetch and import it, which really is the whole install: no peer dependencies, no config, no native build, and TypeScript definitions in the package. The friction is subtler. The types are declared with a reference to the DOM lib, so a Node-only tsconfig without 'dom' in lib can produce type errors for Request and Response. There is no exports map and the package is CommonJS, so bundlers pick the entry through the browser and react-native fields, and any tool that ignores those fields (some SSR and edge runtimes, some jest transforms) can hand a browser build to Node or the reverse. Choosing between the ponyfill and the polyfill import matters for tests: mocking libraries that patch globalThis.fetch will not see calls made through the imported ponyfill.

Patterns

Import fetch without touching globalsponyfill-import

import fetch from 'cross-fetch';

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

This is the recommended form: nothing is assigned to globalThis, so consumers who already have fetch are unaffected. Remember fetch does not reject on 404, so check res.ok yourself.

Install fetch globally as a side effectpolyfill-import

// once, at the top of your entry file
import 'cross-fetch/polyfill';

// anywhere after that
const res = await fetch('/api/health');

The polyfill only assigns when global.fetch is missing, so on Node 18+ it is a no-op and your calls go to the native client instead of node-fetch.

Use it from CommonJScommonjs-require

const fetch = require('cross-fetch');
// or with named parts:
const { Headers, Request, Response } = require('cross-fetch');

require('cross-fetch/polyfill'); // global form

The package is CommonJS with no exports map, so require works directly and default interop is handled inside the wrapper.

POST a JSON bodypost-json

const res = await fetch('https://api.example.com/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ sku: 'A-100', qty: 2 }),
});

const data = await res.json();

No automatic serialization: stringify the body and set Content-Type yourself, the same as with native fetch.

Separate HTTP errors from transport errorserror-handling

try {
  const res = await fetch(url);
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`);
  }
  return await res.json();
} catch (error) {
  // node-fetch wraps network problems in FetchError with a .code
  if (error.code === 'ENOTFOUND') console.error('DNS failure');
  throw error;
}

On Node the rejection is a node-fetch FetchError, not the TypeError native fetch throws, so error-type checks written against native fetch do not match.

Cancel a request with AbortControllerabort-and-timeout

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch(url, { signal: controller.signal });
  return await res.json();
} finally {
  clearTimeout(timer);
}

node-fetch 2.x has no timeout option that the spec recognizes and does not accept AbortSignal.timeout on older Node; the explicit controller plus setTimeout works everywhere.

Build headers with the exported Headers classcustom-headers

import { Headers } from 'cross-fetch';

const headers = new Headers();
headers.append('Authorization', `Bearer ${token}`);
headers.append('Accept', 'application/json');

const res = await fetch(url, { headers });

Import Headers, Request, and Response from cross-fetch rather than assuming globals, otherwise Node without a polyfill throws ReferenceError.

Read a large response as a stream on Nodestream-response-body

import fs from 'node:fs';
import fetch from 'cross-fetch';

const res = await fetch('https://example.com/big.csv');
// res.body is a Node Readable here, not a WHATWG ReadableStream
res.body.pipe(fs.createWriteStream('big.csv'));

This is the biggest portability trap: the same code in a browser needs res.body.getReader(). If you need one streaming API across environments, use undici or native fetch instead.

Send multipart form dataupload-formdata

// browser / React Native: FormData is global
const form = new FormData();
form.append('file', fileOrBlob, 'invoice.pdf');

await fetch('/upload', { method: 'POST', body: form });

// Node with cross-fetch: install form-data and use its headers
// const FormData = require('form-data');
// const form = new FormData();
// form.append('file', fs.createReadStream('invoice.pdf'));
// await fetch(url, { method: 'POST', body: form, headers: form.getHeaders() });

node-fetch 2.x predates the spec FormData, so on Node you need the form-data package and must pass its getHeaders() to set the boundary.

Mock requests in testsmock-in-tests

// jest.setup.js
require('cross-fetch/polyfill');

// test file
import fetchMock from 'jest-fetch-mock';
fetchMock.enableMocks();

test('loads user', async () => {
  fetchMock.mockResponseOnce(JSON.stringify({ id: 1 }));
  await expect(loadUser()).resolves.toEqual({ id: 1 });
});

Global mocks only intercept code that calls globalThis.fetch. Modules that import the ponyfill hold their own reference and bypass the mock entirely, which is the usual reason a mock 'does nothing'.

Fix Request and Response type errors on a Node projecttypescript-config

// tsconfig.json
{
  "compilerOptions": {
    "lib": ["ES2022", "DOM"],
    "types": ["node"]
  }
}

index.d.ts starts with a reference to the DOM lib, so a Node-only lib list makes the exported types resolve to nothing. Adding DOM is the cheap fix.

Remove it on Node 18 and newermigrate-off

// before
import fetch from 'cross-fetch';
const res = await fetch(url);

// after: delete the dependency, fetch is global
const res = await fetch(url);

// need a Node-only client with real streams and test interceptors?
// import { request, MockAgent } from 'undici';

Check for res.body.pipe() calls before deleting: native fetch returns a WHATWG stream, so piping to a file becomes Readable.fromWeb(res.body).pipe(...).

Alternatives

PackageRegistryPick it when
undicinpmYou are Node-only and want the implementation behind native fetch, with real streams, pooling, and mock interceptors for tests
node-fetchnpmYou want the Node shim directly, without the cross-environment indirection, and can use its ESM v3 line
kynpmYou want a small wrapper over whatever fetch already exists, adding retries, timeouts, and hooks instead of replacing the API
axiosnpmYou want one HTTP client with interceptors and progress events across Node and browsers, and do not need the fetch API shape