Quick verdict: JavaScript has no native cURL — cURL is a system binary, not a JS library. The three alternatives in 2026 are fetch (built into browsers and Node 18+, standards-based), axios (third-party with cleaner API, interceptors, automatic JSON), and shelling out to real curl via child_process when you need exact flag compatibility. For 95% of cases, fetch is the right default.
This guide covers when to pick each, working examples for sync HTTP/POST/headers/body, plus how to add proxy support to all three patterns.
Available in browsers and Node 18+. No install required.
// curl https://api.example.com/items
const r = await fetch("https://api.example.com/items");
const data = await r.json();
// curl -X POST -H "Content-Type: application/json" --data '{"a":1}' https://api.example.com/items
const r = await fetch("https://api.example.com/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ a: 1 }),
});
// curl -H "Authorization: Bearer xyz" https://api.example.com/profile
const r = await fetch(url, { headers: { "Authorization": "Bearer xyz" } });
For Node-specific options like custom agents (proxies, custom CA), import from undici in Node 18+:
import { fetch, ProxyAgent } from "undici";
const proxyAgent = new ProxyAgent("http://USER:[email protected]:8080");
const r = await fetch("https://api.example.com", { dispatcher: proxyAgent });
npm install axios
import axios from "axios";
// GET — JSON parsed automatically
const { data } = await axios.get("https://api.example.com/items");
// POST — JSON serialized automatically, Content-Type set automatically
const r = await axios.post("https://api.example.com/items", { a: 1 });
// Through a proxy
const r = await axios.get("https://api.example.com", {
proxy: {
protocol: "http",
host: "proxy.spyderproxy.com",
port: 8080,
auth: { username: "USER", password: "PASS" },
},
});
// HTTPS-tunneling proxy (use https-proxy-agent for HTTPS targets)
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://USER:[email protected]:8080");
const r = await axios.get("https://api.example.com", { httpsAgent: agent });
axios.interceptors.response.use(
r => r,
async (error) => {
if (error.response?.status === 401) {
const newToken = await refreshAuthToken();
error.config.headers["Authorization"] = `Bearer ${newToken}`;
return axios.request(error.config);
}
throw error;
}
);
This pattern (auth refresh on 401) is much harder to write with bare fetch — it's the main reason axios still has a large user base in 2026.
Use when you need exact curl-flag compatibility — porting curl-heavy bash scripts to Node, or using curl-impersonate for TLS fingerprint bypass.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const exec = promisify(execFile);
// curl -I -L -s https://example.com
const { stdout } = await exec("curl", [
"-I", "-L", "-s",
"-x", "http://USER:[email protected]:8080",
"--max-time", "20",
"https://example.com",
]);
console.log(stdout);
Pros: 100% curl flag compatibility, including HEAD requests, complex timeouts, multipart forms. Cons: slow (~50-100 ms process spawn per call), uses subprocess limits, harder to capture HTTP errors cleanly.
| Use case | Pick |
|---|---|
| Browser-side HTTP | fetch (or axios) |
| Modern Node.js (18+) | fetch |
| Need interceptors / auth refresh | axios |
| Need exact curl-flag compat | child_process + curl |
| TLS fingerprint impersonation (anti-bot) | child_process + curl-impersonate |
| Streaming large downloads | fetch (Response.body) or axios responseType: 'stream' |
| High volume scraping (1000+ req/s) | fetch with undici Pool |