Library / SDK
node-fetch/node-fetch avatar
node-fetch/node-fetch

node-fetch: the Fetch API for Node.js, and what it costs you in v3

A light-weight module that brings the Fetch API to Node.js

8,857 stars1,059 forksJavaScriptMIT

At a glance

What is it?
node-fetch brings window.fetch to the Node runtime with native streams and a WHATWG-compatible surface. The v3 line is ESM-only, which is the decision most teams actually have to make.
Who is it for?
Adopt node-fetch if you are on Node 12.20.0 or newer, want a window.fetch-shaped client, and can live with ESM (v3) or accept the older CommonJS line (v2). Do not adopt it if you need a synchronous HTTP path, are pinned to a Node version below the engines field, or depend on a client-side fetch feature listed in docs/v3-LIMITS.md.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 141 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem node-fetch solves, and who actually has it

Node ships an http module, not a fetch function. Before node-fetch, calling an HTTP endpoint meant assembling request options, listening for data events, concatenating buffers, and handling gzip yourself. The README states the motivation plainly: instead of implementing XMLHttpRequest in Node to run a browser fetch polyfill, go from native http to the fetch API directly. That is the whole pitch, and it is a good one for anyone who writes the same request code in a browser bundle and a server process.

The target user is a backend or full-stack JavaScript developer who already knows the fetch shape from the browser and does not want a second mental model on the server. The package keeps the window.fetch surface, uses native promises and async functions, and uses native Node streams for bodies on both the request and the response side. If your codebase is TypeScript, the package ships @types/index.d.ts and the repository runs tsd as a test script, so the type definitions are part of the release artifact rather than a community afterthought.

It is not a general HTTP toolkit. There is no retry policy, no rate limiter, no connection pool you configure directly beyond passing an agent, and no built-in caching layer. It is the request primitive, and everything around it is yours to build.

How the request pipeline is put together

The package is ESM. package.json sets "type": "module" and points main at ./src/index.js, with the type declarations at ./@types/index.d.ts. The published files list is just src and the declaration file, so what you install is the source you can read.

Requests go out over Node's native http stack, which is why the README can offer a custom agent option and an insecure HTTP parser option. Those two options are the clearest signal that this is not a browser shim: a browser fetch has no concept of an agent or a lenient HTTP parser, and node-fetch exposes both because Node's http module does. Bodies are native Node streams in both directions, so a large upload or download does not have to be buffered into a string first. The README also documents a custom highWaterMark option for tuning that stream buffering.

Content encoding is handled for you. The README states that gzip, deflate and brotli are decoded and that string output such as res.text() and res.json() is converted to UTF-8 automatically. On top of the spec surface the project adds extensions it considers useful: a redirect limit, a response size limit, and explicit error classes (FetchError and AbortError) so a failed request is distinguishable from a failed parse.

The project is explicit that it makes trade-offs against the WHATWG fetch spec and the stream spec rather than claiming perfect conformance. Those trade-offs are written down in docs/v3-LIMITS.md for the 3.x line and docs/v2-LIMITS.md for 2.x. That is the document to read before you assume a browser behaviour carries over.

Installing node-fetch and making a first request

The README gives one install command for the current stable line. The same section states that the 3.x release requires at least Node.js 12.20.0, and package.json widens the engines field to ^12.20.0 || ^14.13.1 || >=16.0.0. Check your runtime against that before anything else.

bash
npm install node-fetch

Because v3 is ESM-only, the import is the ESM form. This is the exact snippet the README shows for loading the module.

js
import fetch from 'node-fetch';

A first real request is the plain-text example from the README. It fetches a URL, reads the body as text, and logs it.

js
import fetch from 'node-fetch';

const response = await fetch('https://github.com/');
const body = await response.text();

console.log(body);

The JSON example is the same shape with a different reader, and it is the one most services will use.

js
import fetch from 'node-fetch';

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

If your project is still CommonJS, the README is direct about the constraint: node-fetch from v3 is an ESM-only module and you cannot import it with require(). The documented escape hatches are installing node-fetch@2, which remains CommonJS-compatible and still receives critical bug fixes, or wrapping the dynamic import.

js
// mod.cjs
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));

The README also shows a global patch file that assigns fetch, Headers, Request and Response onto globalThis when they are absent, which is how you keep browser-shaped code working unchanged on the server.

The ESM-only decision is the real adoption cost

The most consequential thing about node-fetch v3 is not the fetch surface. It is that the package is ESM-only, and the README says so in a sentence that will stop some upgrades cold. Any dependency chain that reaches node-fetch through require() breaks on v3. That includes older bundler configurations, some test runners, and any CommonJS module in your tree that imports it transitively.

The documented path out is v2, and the README frames it as a supported one: v2 remains compatible with CommonJS and critical bug fixes will continue to be published for it. That is a genuine commitment, but it also means two live documentation trees. The README links a separate 2.x README, a 2.x to 3.x upgrade guide, and a 1.x to 2.x guide, and it warns that the usage documentation below the upgrade section is written for 3.x releases. If you are on v2 and reading the main README, you are reading the wrong page.

There is a second cost that is easy to miss. The repository's last push was on 2026-05-12, while the most recent releases listed are v2.7.0 and v2.6.13 from 2023-08-23 and 2023-08-18, and v3.3.2 from 2023-07-25. Repository activity and published releases are not the same thing, and anyone pinning to a version should look at the release list rather than assuming a recent commit means a recent tag. The README does not document a rollback procedure for a failed upgrade, so plan the version pin before you start.

Where node-fetch is the wrong tool

If your Node version is 18 or newer, you may not need this package at all. Modern Node exposes a global fetch, and the README's own global-patching example is guarded by if (!globalThis.fetch), which acknowledges that the global may already exist. Installing node-fetch on a runtime that has a native implementation adds a dependency to get a second implementation of the same interface. The project's own documentation lists known differences from window.fetch in docs/v3-LIMITS.md, and those differences will not match the native implementation's differences, so mixing the two across a codebase is a good way to get inconsistent behaviour.

node-fetch is also the wrong tool if you need features outside the fetch model. There is no built-in retry, no circuit breaker, no request queue. The README documents a redirect limit and a response size limit as extensions, which tells you the project is willing to add knobs, but it does not turn the module into a client with policy. If your requirements include automatic retries with backoff or per-host concurrency limits, you are building that on top.

Finally, the error surface is explicit and that has a cost. FetchError and AbortError are separate classes, and the README has dedicated sections for handling exceptions and for handling client and server errors. A fetch call does not reject on a 404 or a 500; you check response.ok. Teams coming from clients that throw on non-2xx status codes routinely ship code that silently treats an error page as a successful response. The README's separate section on response.ok exists because that mistake is common.

node-fetch against axios, and against native fetch

The comparison people actually search for is node-fetch versus axios, and the two differ at the level of the interface, not the feature list. axios gives you a client object with defaults, interceptors, and automatic JSON transformation, and it rejects on non-2xx status. node-fetch gives you a single function that mirrors window.fetch, returns a Response you inspect yourself, and leaves defaults and interception to you. If your team already writes fetch in the browser, node-fetch means one API to learn. If your team wants a configured client with interceptors, axios is the closer fit and node-fetch will feel like a lower-level primitive.

The more interesting comparison is node-fetch against the fetch that Node now ships natively. The project's own motivation section frames the package as the way to get a window.fetch compatible API on the Node runtime, and that framing predates a global fetch existing. On a runtime with a native global, the honest question is whether you need the package's extensions: the redirect limit, the response size limit, the custom agent, the custom highWaterMark, the insecure HTTP parser, and the FetchError and AbortError classes. If you use none of those, the native global covers the same surface. If you need to route requests through a proxy agent or cap response size, node-fetch exposes options that the native global does not document.

Licence, maintenance and what an upgrade really costs

The licence is MIT, stated in package.json and in LICENSE.md at the repository root. MIT is permissive: it allows use, modification and redistribution provided the copyright notice and permission notice are retained. That is the extent of what the repository tells you, and it is not legal advice. If you redistribute node-fetch inside a bundled artifact, the notice requirement is the part that actually applies to you, and your legal team should confirm how your build handles third-party notices.

Maintenance signals are mixed and worth reading carefully. The repository is not archived, and the last push was on 2026-05-12. The releases listed are older: v3.3.2 on 2023-07-25, v2.7.0 on 2023-08-23, and v2.6.13 on 2023-08-18. The README commits to continuing critical bug fixes for v2, which is a statement about the 2.x line specifically rather than about the project as a whole.

The upgrade cost between majors is documented rather than incidental. Moving from 2.x to 3.x means reading docs/v3-UPGRADE-GUIDE.md and accepting the ESM constraint. Moving from 1.x to 2.x has its own guide. The changelog lives in the GitHub releases rather than in the repository tree. For a dependency this central to a service's outbound traffic, the practical cost is not the install command; it is auditing every call site for the require() pattern and confirming the Node version in your deployment images against the engines field.

Editorial conclusion

Adopt node-fetch if you are on Node 12.20.0 or newer, want a window.fetch-shaped client, and can live with ESM (v3) or accept the older CommonJS line (v2). Do not adopt it if you need a synchronous HTTP path, are pinned to a Node version below the engines field, or depend on a client-side fetch feature listed in docs/v3-LIMITS.md. Before committing, check your Node version against the engines field, read docs/v3-UPGRADE-GUIDE.md if you are moving from 2.x, and confirm whether require() appears anywhere in the call path, because that alone decides which major you install.

Frequently asked questions

How do I install node-fetch?

Run npm install node-fetch for the current 3.x line, which requires at least Node.js 12.20.0. If your project uses CommonJS and cannot switch to ESM, the README points you to npm install node-fetch@2 instead.

How do I use node-fetch?

Import fetch from node-fetch, call it with a URL, then read the body with a method such as response.text() or response.json(). The README shows both the plain-text and JSON examples as short async snippets.

Is node-fetch deprecated?

The repository is not archived, and the README states that critical bug fixes will continue to be published for v2. The README also notes that v3 is ESM-only, so the reason to stay on v2 is module format rather than deprecation.

What is node-fetch used for?

It brings the Fetch API to Node.js so server code can use the same window.fetch style interface as browser code. The README describes it as a light-weight module built directly on Node's native http rather than on an XMLHttpRequest polyfill.

How do I use a proxy in node-fetch?

The README documents a custom agent option, which is how you route requests through a Node agent such as a proxy agent. It does not document a dedicated proxy URL option, so the agent is the mechanism.

What is node-fetch?

It is a light-weight module that brings the Fetch API to Node.js, keeping the window.fetch interface while using native promises and native Node streams for request and response bodies. The README describes it as minimal code for a window.fetch compatible API on the Node runtime.

Official sources

  1. Issues
  2. License: MIT
  3. node-fetch/node-fetch on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/node-fetch-node-fetch.svg)](https://hysenlabs.com/projects/node-fetch-node-fetch)