Fetching Data from APIs in Node.js
If you’re a frontend developer writing Node.js code, you’ve probably reached for Axios out of habit, or wondered whether you still need node-fetch. The answer today is simpler than you might think: modern Node.js has a stable, built-in fetch API, and for most use cases, it’s all you need.
Here’s what you need to know to make clean, reliable server-side HTTP requests today.
Key Takeaways
- Modern Node.js ships with a global
fetchAPI powered by undici — no packages to install. fetchdoes not throw on HTTP errors like 404 or 500. Always checkresponse.okbefore reading the body.- Use
AbortSignal.timeout()for request timeouts instead of manualAbortControllerwiring. - For high-throughput scenarios with repeated requests to the same origin, use undici’s
Poolfor connection reuse. - Prefer native
fetchover Axios in Node-only projects to keep your dependency list lean.
The Node.js Fetch API Is Stable and Built In
Since Node.js 18, fetch is available globally with no imports required. It was marked experimental in v18, then stabilized in v21. It’s powered by undici under the hood — the HTTP client that backs Node’s fetch implementation — and it mirrors the browser Fetch API you already know.
No installation. No require('node-fetch'). Just fetch().
const response = await fetch("https://api.github.com/users/nodejs/repos", {
headers: { "User-Agent": "my-app" },
})
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const repos = await response.json()
console.log(repos.map((r) => r.name))
One important thing to understand: fetch does not throw on HTTP errors like 404 or 500. It only throws on network failures. Always check response.ok or response.status before reading the body.
Making POST Requests with the Node.js Fetch API
Sending data is straightforward. Set method, add a Content-Type header, and serialize your payload:
const response = await fetch("https://api.example.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Hello", body: "World" }),
})
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const data = await response.json()
For FormData uploads, skip the Content-Type header entirely — fetch sets it automatically with the correct multipart boundary.
Handling Timeouts with AbortSignal
There’s no built-in timeout option on the fetch call itself, but AbortSignal.timeout() makes it clean:
const response = await fetch("https://api.example.com/data", {
signal: AbortSignal.timeout(5000), // 5 seconds
})
If the request exceeds the limit, the request is aborted. No manual AbortController setup needed for the simple case.
For more complex scenarios — say you want to cancel a request based on user action and enforce a timeout — you can combine an AbortController with AbortSignal.any():
const controller = new AbortController()
const response = await fetch("https://api.example.com/data", {
signal: AbortSignal.any([
controller.signal,
AbortSignal.timeout(5000),
]),
})
// Call controller.abort() elsewhere to cancel manually
Discover how at OpenReplay.com.
When to Use Undici Directly
For most Node.js API requests, global fetch is the right tool. But if you’re making many requests to the same origin — like hitting an internal service repeatedly — undici’s Pool gives you connection reuse and finer control.
import { Pool } from "undici"
const pool = new Pool("https://internal-api.example.com", {
connections: 10,
})
const { statusCode, body } = await pool.request({
path: "/data",
method: "GET",
})
const data = await body.json()
Note that pool.request() returns a body that is a readable stream, not a Response object. You need to consume it explicitly — for example with body.json() or body.text().
Use undici directly when you need advanced pooling, streaming large responses efficiently, or maximum throughput in performance-sensitive code.
Node.js Fetch vs Axios: Which Should You Use?
| Native Fetch | Axios | |
|---|---|---|
| Install required | No | Yes |
| Browser compatible | Yes | Yes |
| Auto JSON parse | No (call .json()) | Yes |
| Interceptors | Manual | Built-in |
| Throws on HTTP errors | No | Yes |
| Timeout option | AbortSignal | Built-in |
Axios remains a solid choice if you want interceptors, automatic JSON parsing, or consistent behavior across browser and Node in the same codebase. But if you’re writing Node-only code and don’t need those extras, native fetch keeps your dependency list clean.
Avoid node-fetch for new projects. It was the right solution before Node 18, but it’s now redundant on any supported Node.js version.
Conclusion
For server-side HTTP requests in modern Node.js, start with the built-in fetch. It’s stable, familiar, and requires nothing extra. Add AbortSignal.timeout() for timeouts, check response.ok for errors, and reach for undici’s Pool only when performance demands it. Keep Axios in your toolkit if you genuinely need its higher-level features — but don’t install it by default.
FAQs
Yes. The global fetch API was introduced as experimental in Node.js 18 and became stable in Node.js 21. It is powered by undici and requires no additional packages. Any currently supported Node.js version includes it out of the box.
By design, fetch only rejects the promise on network-level failures such as DNS resolution errors or lost connections. HTTP error status codes like 404 or 500 are considered valid responses. You must check response.ok or response.status yourself before processing the body.
Yes, there is no conflict. Some teams use native fetch for simple requests and Axios where they need interceptors or automatic error throwing. That said, mixing HTTP clients adds complexity, so pick one as your default and use the other only when its specific features are needed.
Use undici directly when you need connection pooling, fine-grained control over HTTP connections, or maximum throughput for repeated requests to the same origin. For typical API calls where simplicity matters more than raw performance, the global fetch is sufficient.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.