axios, got, node-fetch, and @hyper-fetch/core are all tools designed to handle HTTP requests in JavaScript, but they target different environments and architectural needs. axios is a universal client that works in both browsers and Node.js, famous for its automatic JSON transformation and broad ecosystem. got is a streamlined, powerful HTTP client built specifically for Node.js, focusing on developer experience and advanced features like retry logic. node-fetch brings the native browser fetch API to the Node.js environment, offering a lightweight standard-compliant interface. @hyper-fetch/core is a specialized toolkit designed to simplify complex fetch configurations, particularly for large-scale applications needing robust error handling, caching, and request deduplication without the boilerplate.
Making HTTP requests is a daily task for JavaScript developers, but the tool you choose shapes your code's readability, error handling, and deployment strategy. While fetch is now native in modern Node.js and browsers, the ecosystem offers specialized tools like axios, got, node-fetch, and @hyper-fetch/core to solve specific pain points. Let's break down how they differ in real-world scenarios.
The first decision is where your code runs. Some libraries bridge the gap between browser and server, while others double down on server-specific features.
axios is universal. It uses XMLHttpRequest in the browser and the native http module in Node.js, giving you the same API everywhere.
// axios: Works in Browser and Node.js
import axios from 'axios';
const response = await axios.get('/api/users');
console.log(response.data); // Automatically parsed JSON
got is strictly for Node.js. It leverages Node-specific streams and events, making it impossible to use directly in the browser.
// got: Node.js only
import got from 'got';
const response = await got.get('https://api.example.com/users');
console.log(response.body); // Automatically parsed JSON
node-fetch brings the browser fetch API to older Node.js versions (pre-18). It mimics the standard web API exactly.
// node-fetch: Node.js implementation of Web Standard
import fetch from 'node-fetch';
const response = await fetch('https://api.example.com/users');
const data = await response.json(); // Manual JSON parsing
@hyper-fetch/core wraps the native fetch (or node-fetch) to add structure. It assumes you are comfortable with the standard fetch API but need help managing it at scale.
// @hyper-fetch/core: Enhances native fetch
import { createFetcher } from '@hyper-fetch/core';
const fetcher = createFetcher({ baseURL: 'https://api.example.com' });
const response = await fetcher.get('/users');
const data = await response.json(); // Follows standard fetch response
How much work do you want to do to get your data? Some libraries guess what you want; others make you explicit.
axios automatically transforms JSON responses. You access data via the .data property. This saves lines of code but can sometimes hide network errors if the content type is wrong.
// axios: Auto-parsing
try {
const res = await axios.post('/login', { user: 'alice' });
console.log(res.data.token); // Direct access
} catch (error) {
console.error(error.response.status);
}
got also auto-parses JSON by default but gives you more control over the raw body if needed. It returns a unified response object.
// got: Auto-parsing with options
try {
const res = await got.post('https://api.example.com/login', {
json: { user: 'alice' } // Auto-serializes body
});
console.log(res.body.token);
} catch (error) {
console.error(error.response.statusCode);
}
node-fetch and @hyper-fetch/core follow the standard: you must manually call .json() on the response. This is more verbose but makes the data flow explicit and predictable.
// node-fetch: Manual parsing
const res = await fetch('https://api.example.com/login', {
method: 'POST',
body: JSON.stringify({ user: 'alice' }),
headers: { 'Content-Type': 'application/json' }
});
const data = await res.json(); // Explicit step
console.log(data.token);
// @hyper-fetch/core: Manual parsing (Standard compliant)
const res = await fetcher.post('/login', {
body: { user: 'alice' } // Helper handles headers/serialization
});
const data = await res.json(); // Explicit step
console.log(data.token);
Networks are unreliable. How a library handles failures determines how much boilerplate you write in your app.
axios throws errors for 4xx and 5xx status codes automatically. You catch them in a standard try/catch block, but implementing retries requires external plugins or custom interceptors.
// axios: Throws on HTTP errors
try {
await axios.get('/unstable-endpoint');
} catch (err) {
if (err.response) {
// Handle server error
console.log(`Status: ${err.response.status}`);
}
}
got shines here. It has built-in retry logic with exponential backoff and detailed error classes that tell you exactly what went wrong (timeout, DNS error, HTTP error).
// got: Built-in retries
try {
const res = await got.get('https://unstable-api.com/data', {
retry: {
limit: 3,
methods: ['GET']
}
});
} catch (err) {
// err.code tells you specifically why it failed
console.log(`Failed after retries: ${err.code}`);
}
node-fetch does NOT throw on HTTP errors (like 404). It only throws on network failures. You must manually check res.ok.
// node-fetch: Manual error checking
const res = await fetch('https://api.example.com/missing');
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
@hyper-fetch/core is designed to solve the node-fetch verbosity problem. It allows you to define global strategies for errors and retries, wrapping the standard API to enforce rules across your whole app.
// @hyper-fetch/core: Centralized error/retry strategy
const fetcher = createFetcher({
strategies: {
retry: { attempts: 3, delay: 1000 },
errors: { throwOn: [400, 500] } // Force throw on specific codes
}
});
// Now behaves more like axios/got regarding errors
await fetcher.get('/unstable-endpoint');
Large apps often need to modify requests (add auth tokens) or responses (log data) globally.
axios uses interceptors. They are powerful and easy to set up for things like attaching JWT tokens.
// axios: Interceptors
axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
});
got uses hooks. These are granular and run at specific stages of the request lifecycle (before request, before redirect, after response).
// got: Hooks
const client = got.extend({
hooks: {
beforeRequest: [
options => {
options.headers.authorization = `Bearer ${getToken()}`;
}
]
}
});
node-fetch has no built-in interceptor system. You typically wrap fetch in your own helper function, which can lead to inconsistent implementations across a team.
// node-fetch: Manual wrapper pattern
async function authFetch(url, options = {}) {
const token = getToken();
return fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` }
});
}
@hyper-fetch/core provides a formalized system for this. It lets you define "strategies" that act like interceptors but are typed and enforced, preventing the "wild west" of custom wrappers.
// @hyper-fetch/core: Strategy-based configuration
const fetcher = createFetcher({
strategies: {
request: [
(req) => {
req.headers.set('Authorization', `Bearer ${getToken()}`);
return req;
}
]
}
});
It is critical to note the current status of these packages before starting a new project.
node-fetch: While still widely used, note that Node.js 18+ includes a global native fetch. For new projects targeting modern Node versions, you likely do not need to install node-fetch anymore unless you need specific polyfill behavior for older environments. The package itself is in maintenance mode.axios, got, and @hyper-fetch/core are actively maintained and safe for new projects.| Feature | axios | got | node-fetch | @hyper-fetch/core |
|---|---|---|---|---|
| Environment | Browser & Node | Node.js Only | Node.js (Polyfill) | Browser & Node (via fetch) |
| JSON Parsing | Automatic (.data) | Automatic (.body) | Manual (.json()) | Manual (.json()) |
| Error Handling | Throws on 4xx/5xx | Throws + Detailed Codes | Only throws on Network | Configurable Strategies |
| Retries | External Plugins | Built-in | Manual | Built-in Strategies |
| Config Style | Interceptors | Hooks | Manual Wrappers | Strategies |
| Bundle Size | Medium | Small | Very Small | Small (plus fetch) |
axios remains the safe, universal choice. If you need one library to rule both client and server, or if your team relies on its automatic JSON handling and mature interceptor system, it is still a solid pick. It removes friction for standard CRUD apps.
got is the power user's choice for Node.js. If you are building backend services, CLI tools, or serverless functions, got offers a superior developer experience with better error messages, built-in retries, and a cleaner API than axios. Do not use it in the browser.
node-fetch (or the native fetch in Node 18+) is for purists. If you want to write code that looks exactly like browser code and keep dependencies minimal, this is the way. Be prepared to write more boilerplate for error handling and retries.
@hyper-fetch/core is the architect's choice for scaling fetch. If you love the standard fetch API but hate repeating error handling, retry logic, and header management across hundreds of files, this library provides the guardrails you need. It turns fetch from a simple tool into an enterprise-grade solution without abandoning web standards.
Final Thought: For modern greenfield projects, the trend is shifting toward native fetch (enhanced by tools like @hyper-fetch/core if needed) for frontend and got for backend. axios remains relevant for universal codebases, but understanding its abstraction cost is key.
Choose @hyper-fetch/core if you are building a large-scale application where managing raw fetch calls has become unmanageable due to repetitive boilerplate for caching, retries, and error handling. It is ideal for teams that want to stick with the native fetch API standard but need an architectural layer to enforce consistency, handle global interceptors, and manage request lifecycles centrally without adding heavy abstraction.
Choose axios if you need a single library that works identically in both the browser and Node.js environments, especially for legacy projects or teams that rely heavily on automatic JSON parsing and request/response interception. It is the safest bet for projects that require broad compatibility, mature community support, and features like CSRF protection out of the box, though it comes with a larger bundle size compared to native solutions.
Choose got if your application runs exclusively in Node.js (server-side) and you need a feature-rich, ergonomic alternative to the built-in http module or fetch. It is perfect for backend services, CLI tools, or scripts that require advanced capabilities like automatic retries, pagination helpers, and detailed error messages, offering a more modern and concise API than axios for server-only contexts.
Choose node-fetch if you are working in a Node.js environment (versions 18 and below) and want to use the standard browser fetch API syntax without pulling in a heavy universal client like axios. It is the best choice for lightweight microservices or scripts where adhering to web standards is a priority, provided you do not need the advanced convenience features found in got or axios.
One SDK for every API. The type-safe API layer for TypeScript apps — REST, GraphQL, WebSockets, SSE, Firebase, and more.
HyperFetch Core is the foundation of the HyperFetch ecosystem. Every API call is a typed, immutable request object with built-in caching, queuing, retries, and offline support. It works in any JavaScript environment — browser, server, or edge — and connects to any API through pluggable adapters.
any againnpm install @hyper-fetch/core
import { createClient } from "@hyper-fetch/core";
// Single entry point — all requests inherit this base URL
const client = createClient({ url: "https://api.example.com" });
// Define a request with typed response — :userId becomes a required param
const getUser = client.createRequest<{ response: { id: number; name: string } }>()({
endpoint: "/users/:userId",
method: "GET",
});
// setParams is typed from the endpoint string, send() returns typed data
const { data, error } = await getUser.setParams({ userId: 1 }).send();
interface User {
id: number;
name: string;
email: string;
}
// GET request — only response type needed, params inferred from :userId
const getUser = client.createRequest<{ response: User }>()({
endpoint: "/users/:userId",
method: "GET",
});
// POST request — define both response and payload types
const createUser = client.createRequest<{
response: User;
payload: { name: string; email: string };
}>()({
endpoint: "/users",
method: "POST",
});
// Params are type-checked: { userId: number } required here
const { data } = await getUser.setParams({ userId: 1 }).send();
// Payload is type-checked: { name, email } required here
const { data: newUser } = await createUser.send({
data: { name: "Jane", email: "jane@example.com" },
});
// Define allowed query params — all are optional and type-checked
const listUsers = client.createRequest<{
response: User[];
queryParams: { page?: number; limit?: number; search?: string };
}>()({
endpoint: "/users",
method: "GET",
});
// Query params are appended to the URL: /users?page=1&limit=20&search=john
const { data } = await listUsers.setQueryParams({ page: 1, limit: 20, search: "john" }).send();
// Hook into the request lifecycle — track progress, log events, handle responses
const { data } = await getUser.setParams({ userId: 1 }).send({
onStart: ({ requestId }) => console.log(`Request ${requestId} started`),
onResponse: ({ response }) => console.log("Got data:", response.data),
onUploadProgress: ({ progress }) => console.log(`Upload: ${progress}%`),
onDownloadProgress: ({ progress }) => console.log(`Download: ${progress}%`),
});
// Transform the response before it reaches your code
const getUser = client
.createRequest<{ response: User }>()({
endpoint: "/users/:userId",
method: "GET",
})
.setResponseMapper((response) => ({
...response,
data: { ...response.data, name: response.data.name.toUpperCase() },
}));