HTTP Requests in Node.js With the Fetch API
Use Node.js’s built-in fetch to make HTTP requests, send JSON, handle errors, parse responses, and set timeouts—with runnable examples and troubleshooting tips.
On modern Node.js, make HTTP requests with the global fetch() function. Check response.ok yourself—HTTP errors such as 404 do not reject the promise—then read the response body with the method that matches its format. Add an AbortSignal when the request needs a deadline.
1. Is fetch built into Node.js?
Yes. Node.js provides a browser-compatible global fetch(), based on Undici. It was added in Node v17.5.0 and v16.15.0; the experimental flag was no longer required in v18.0.0, and fetch was no longer experimental in v21.0.0. The same global API reference documents related globals such as Headers, Request, Response, and FormData. Check your deployed Node version if a runtime reports that fetch is undefined. Node.js global fetch documentation
For an ordinary API request, you do not need to install a package:
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
console.log(data);
Save this as request.mjs and run node request.mjs. The .mjs extension enables top-level await. In a CommonJS file, put the same code inside an async function or an async IIFE.
2. Make a request and handle its response
fetch(input, init) accepts a URL string, a URL, or a Request. The optional init object configures the request. Its promise fulfills with a Response once response headers are available; that does not mean the status indicates success or that the entire body has been read. Undici Fetch documentation
Check status before treating a response as successful
Network failures reject the promise. HTTP status failures such as 404 generally do not: inspect response.ok, which is true for status codes from 200 through 299. When diagnosing or reporting a failure, use response.status and, where useful, response.statusText and response.headers.
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
Read the body in the right format
A response body is a stream and should be consumed deliberately. Choose one body reader that fits the payload:
| Body method | Use it for |
|---|---|
response.json() |
JSON; parses the body as JSON. |
response.text() |
Plain text, HTML, or an error payload you want to inspect as text. |
response.arrayBuffer() |
Binary data you need in memory as an ArrayBuffer. |
response.blob() |
Binary data represented as a Blob. |
response.formData() |
A response encoded as multipart form data. |
Do not call multiple body readers on the same response after one has consumed it. If you need two independent reads, call response.clone() before reading the body and consume each copy once.
3. Set headers, query parameters, and methods
Use the method field for methods such as GET, POST, PUT, and DELETE. Set request headers with a plain object or a Headers instance. For query parameters, use URL and URLSearchParams so values are encoded correctly instead of concatenating unescaped strings.
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', 'node fetch');
url.searchParams.set('limit', '10');
const response = await fetch(url, {
method: 'GET',
headers: {
accept: 'application/json',
authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const results = await response.json();
Keep credentials out of source control and avoid logging authorization headers. Send only headers the target API requires.
4. Send JSON with POST
Serialize an object with JSON.stringify() and set the content type to application/json. The response may be JSON, but do not assume every successful response has a body: an endpoint can return an empty response. Handle that API contract explicitly.
const response = await fetch('https://api.example.com/items', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({ name: 'example' }),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`HTTP ${response.status}: ${detail}`);
}
const contentType = response.headers.get('content-type') ?? '';
const created = contentType.includes('application/json')
? await response.json()
: await response.text();
console.log(created);
The example reads an error body as text so it can include the server’s response in the error. In production, avoid returning sensitive server details to end users; log an appropriately redacted diagnostic instead.
5. Add a timeout or cancel a request
Pass an AbortSignal through signal. AbortSignal.timeout(milliseconds) creates a signal that aborts after the requested delay. An AbortController is useful when application logic needs to cancel a request before its deadline, such as when a user leaves a page or a job is superseded. Node.js AbortSignal.timeout()
const url = 'https://api.example.com/data';
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
console.log(await response.json());
} catch (error) {
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
console.error('The request was cancelled or timed out.');
} else {
throw error;
}
}
You can use a controller when cancellation is triggered by your own code:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5_000);
try {
const response = await fetch('https://api.example.com/data', {
signal: controller.signal,
});
console.log(response.status);
} finally {
clearTimeout(timer);
}
Aborting fetch also matters when you stop consuming a response body. Set deadlines that fit the operation: a short lookup and a large download may need different limits.
6. Redirects, binary data, and streaming
Choose redirect behavior deliberately
Fetch supports redirect modes: follow (the usual default), error (fail if the response redirects), and manual (expose redirect handling to the caller). Use error when redirects are not acceptable for the request’s security or API semantics. Be careful when handling redirects involving credentials or untrusted URLs. Undici Fetch documentation
const response = await fetch('https://api.example.com/data', {
redirect: 'error',
});
Download binary data
For modest files that fit in memory, read the body as an ArrayBuffer and write a Buffer:
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://example.com/file.bin');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const bytes = Buffer.from(await response.arrayBuffer());
await writeFile('file.bin', bytes);
For large downloads, buffering the entire response consumes memory proportional to the file size. Prefer a streaming approach when the payload is large; Node’s lower-level HTTP API exposes Node streams and avoids buffering entire messages. See the Node.js HTTP documentation for that lower-level interface.
7. cURL and Python equivalents
If you are checking the same endpoint outside Node.js, these examples show the corresponding request shapes. They use a placeholder URL; substitute an endpoint and credentials that you control.
curl -i 'https://api.example.com/data'
import requests
response = requests.get('https://api.example.com/data', timeout=5)
response.raise_for_status()
data = response.json()
print(data)
In Node.js, use the built-in Fetch example above; no package installation is required on supported Node versions.
8. When to use Undici or node:http instead
Use the Fetch API as the clear default for ordinary API calls. Consider lower-level APIs when you need transport or socket controls that Fetch does not expose directly, or when you need a different stream interface.
| Approach | Use when | Trade-off |
|---|---|---|
Global fetch |
Typical JSON APIs, simple downloads, standard request and response handling. | HTTP errors require checking response.ok; bodies use Web API readers and streams. |
| Undici dispatcher or client APIs | You need supported connection configuration or lower-level Undici client controls. | More transport detail to configure; lower-level response bodies need deliberate consumption. |
node:http |
You need low-level request, socket, or Node stream lifecycle controls. | More manual handling of request and response events and streams. |
Node documents that fetch accepts an Undici-compatible custom dispatcher, and that Undici’s setGlobalDispatcher() can change the global dispatcher. A narrowly scoped example using a custom Agent is:
import { Agent } from 'undici';
const dispatcher = new Agent({
connect: { rejectUnauthorized: false },
});
const response = await fetch('https://api.example.com/data', { dispatcher });
Disabling certificate verification weakens TLS security; do not use that setting as a general fix for certificate errors. Configure connection behavior only when you understand the consequence and control the environment. Node.js custom dispatcher documentation
The node:http module is intentionally low-level and designed for the full range of HTTP applications. Its interfaces handle message parsing and stream handling rather than providing Fetch’s higher-level body readers. Node.js HTTP API
9. Troubleshooting common fetch errors
| Symptom | Cause | Fix |
|---|---|---|
fetch is not defined |
The runtime is older or launched with a Node version that does not provide the global. | Check node --version and use a supported modern runtime. The API history lists when fetch arrived and when it became stable. |
| The code succeeds despite a 404 or 500 | Fetch fulfilled with an HTTP response; HTTP status errors do not by themselves reject. | Check response.ok or response.status before treating the result as success. |
SyntaxError from response.json() |
The body is empty, not JSON, or malformed JSON. | Check the endpoint’s response contract and content type; inspect the body with text() when appropriate. Read the body only once. |
Body is unusable or body already read |
A body reader has already consumed the response body. | Choose one reader, or clone the response before the first read if two consumers are genuinely needed. |
| Request hangs longer than expected | No deadline was set, or the chosen deadline is too long. | Pass AbortSignal.timeout() or an AbortController signal and handle cancellation. |
| Request aborts immediately | The signal was already aborted, its timeout is too short, or shared cancellation logic fired. | Create a fresh signal for the request and inspect the code that owns its controller or timeout. |
| Unexpected redirect response or redirect failure | The server redirected and the selected redirect mode affects handling. | Inspect the endpoint URL and choose follow, manual, or error intentionally. |
| TLS certificate error | The peer’s certificate could not be verified in the current environment. | Fix the certificate or trusted CA configuration. Do not broadly disable certificate verification. |
| Request body rejected by server | JSON was not serialized, or the content type does not match the payload. | Use JSON.stringify(value) and set content-type: application/json for JSON. |
10. Performance, reliability, and cost
- Read only what you need. JSON/text convenience methods buffer and parse the body. For large payloads, account for memory use and prefer streaming where appropriate.
- Set deadlines. A bounded request can prevent a stalled dependency from holding work indefinitely. Choose timeout values based on the endpoint and operation.
- Check status and parse failures separately. Network rejection, aborts, non-2xx responses, and invalid response bodies are different failure cases. Handle and log them distinctly.
- Retry with care. A retry after a network error does not prove that a server did not process a request. For operations with side effects, use the API’s idempotency mechanism where available and follow its retry guidance.
- Account for the remote service. Fetch itself has no per-request Node.js package fee when using the built-in global, but the API you call may impose usage limits or charges. Check that service’s terms and pricing.
No performance benchmark is asserted here: request time and resource use depend on the network, remote server, response size, and how the application consumes the body.
11. Or skip the browser setup
If your Node workflow needs a webpage screenshot rather than an HTTP API response, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request returns a PNG, JPEG, WebP, or PDF. Add the request from Node.js with the built-in fetch API:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));
See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free for 1,000 screenshots a month, no card required.
12. FAQ
Does fetch require an npm package in current Node.js?
No. The global is built into modern Node.js. Older versions may not provide it by default; check the version history in the official globals documentation.
Does fetch reject for a 404 response?
No. It normally fulfills with a Response. Check response.ok or response.status.
Can I reuse a response body?
Each body can be consumed once. Clone the response before consuming it if you need a second reader.
Should I always use fetch instead of node:http?
For everyday API calls, fetch is a concise default. Use Undici controls or node:http when you need the lower-level connection or stream control those APIs provide.


