Almost every JSON document a front-end or Node.js service handles arrives through an HTTP request. In this lesson you will request JSON with the standard fetch API, send JSON bodies with the right headers, deal with the failure modes that fetch leaves to you, and see how the popular axios library packages the same steps.
fetch is built into browsers, Node.js 18+, Deno and Bun. It returns a Response whose body is a stream; response.json() reads that stream to the end and parses it:
const res = await fetch("https://api.example.com/users/1");
if (!res.ok) {
throw new Error(`Request failed with status ${res.status}`);
}
const user = await res.json();
console.log(user.name);Two behaviours surprise newcomers. First, fetch only rejects on network failure; a 404 or 500 resolves normally, so you must check res.ok (true for status 200–299) yourself. Second, the body can be consumed only once: calling res.json() after res.text() throws TypeError: Body is unusable. Use res.clone() if you genuinely need to read it twice.
res.json() rejects with a SyntaxError when the body is not valid JSON. The usual cause is a server that answered an error with an HTML page, which is why checking the Content-Type header before parsing, as the helper at the top of this lesson does, produces far clearer errors.
To send JSON you serialize the value yourself and declare the media type. Both steps are required: a plain object passed as body is converted to the string "[object Object]", and without Content-Type: application/json server-side body parsers such as express.json() ignore the body entirely.
const res = await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ sku: "A-1", qty: 2 })
});
const created = await res.json(); // e.g. { id: 501, sku: "A-1", qty: 2, status: "pending" }The optional Accept header tells the server which format you want back. PUT and PATCH work identically; DELETE usually has no body.
axios is a promise-based HTTP client for browsers and Node.js that automates the JSON chores. Install it with npm install axios.
import axios from "axios";
const api = axios.create({ baseURL: "https://api.example.com", timeout: 5000 });
const { data: user } = await api.get("/users/1");
const { data: created } = await api.post("/orders", { sku: "A-1", qty: 2 });
try {
await api.get("/users/999");
} catch (err) {
console.log(err.response?.status, err.response?.data); // 404 { message: "Not found" }
}| Concern | fetch | axios |
|---------|-------|-------|
| Parse JSON response | await res.json() | automatic, in response.data |
| Send an object | JSON.stringify + Content-Type header | automatic |
| Non-2xx status | resolves; check res.ok | rejects with err.response |
| Timeout | AbortSignal.timeout(ms) | timeout option |
| Bundle size | zero, built in | about 30 kB minified |
Neither is "better". fetch needs no dependency and is what frameworks like Next.js extend with caching; axios saves boilerplate and offers interceptors for adding auth headers or logging every request in one place.
res.json() returns whatever the server sent. In TypeScript, const user: User = await res.json() is only a promise to the compiler, not a runtime check; a renamed field on the server still produces undefined in your code. For any API you do not control, validate the parsed value against a JSON Schema or a runtime validator such as Zod before using it.
Also remember that numbers are parsed as IEEE doubles. An API that returns 64-bit IDs as bare numbers, such as {"id": 9007199254740993}, has already lost precision by the time res.json() resolves. Ask for string IDs, or read res.text() and parse with a reviver that uses the raw source.
await on res.json(). It returns a promise; logging it prints Promise { <pending> }.fetch as success. Check res.ok or the status code before parsing.text/plain; always set Content-Type: application/json.AbortSignal and time out long calls.What does `fetch` do when the server responds with status 500?
await res.json() parses a response body; it works once per response and rejects on non-JSON content.fetch resolves on any HTTP status, so check res.ok before parsing.body: JSON.stringify(obj) and Content-Type: application/json.err.response.Next lesson: JSON and REST APIs — how JSON request and response bodies, status codes and resource design fit together in real APIs.