Intermediate · Connecting frontend and backend · topic 17 of 34 · 30 min
Calling the API with fetch
Explanation
What fetch does
fetch(url, options) is built into every modern browser (and into Node.js). It sends one HTTP request and gives back a Promise: a value that arrives later. With await you wait for it and get a Response object, which holds the status code, the headers and the body.
const res = await fetch('/api/notes');
console.log(res.status); // 200
const data = await res.json(); // { notes: [...] }
console.log(data.notes.length);Sending JSON to the API
const res = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Physics formulas', body: 'V = IR', pinned: true }),
});
const note = await res.json(); // the new note, status 201- method: GET is the default. Write POST, PATCH or DELETE yourself.
- headers: Content-Type: application/json tells Express that the body is JSON. Without it, express.json() skips the body and req.body stays empty.
- body: a string. Passing a plain object sends the text "[object Object]", which is a common beginner bug.
fetch does not reject on 404 or 500
fetch rejects (throws when you await it) only when no answer arrives at all: the network is down, the server name cannot be found, the browser blocks the request, or you cancel it. For any answer from the server, even 404 or 500, fetch succeeds. You must check res.ok (true for status 200 to 299) or res.status yourself.
| Status | res.ok | What the client should do |
|---|---|---|
| 200 OK | true | Read the JSON with res.json() |
| 201 Created | true | Read the new note from the body |
| 204 No Content | true | Do NOT call res.json(): there is no body |
| 400 Bad Request | false | Read { error } and show error.message next to the form |
| 401 Unauthorized | false | Send the user to the login page |
| 404 Not Found | false | Show "Note not found" |
| 500 Server Error | false | Show a friendly "Please try again" message |
Reading the contract's error shape
Our Notes API answers every error as { "error": { "code": "...", "message": "..." } }. The client reads error.code to decide what to do (for example UNAUTHORIZED means go to login) and shows error.message to the person. Because the shape is always the same, one piece of code handles every error.
One API client for every screen
An API client is a small file with one function that every screen uses to talk to the server. It is the one place for the base URL, the JSON headers, cookies and error handling. If something changes later, you change one file instead of twenty components.
// src/api.js
const BASE = import.meta.env.VITE_API_URL ?? '/api';
export async function api(path, { method = 'GET', body } = {}) {
const init = { method, credentials: 'include' };
if (body !== undefined) {
init.headers = { 'Content-Type': 'application/json' };
init.body = JSON.stringify(body);
}
const res = await fetch(BASE + path, init);
if (res.status === 204) return null;
if (res.ok) return res.json();
let data = null;
try { data = await res.json(); } catch { /* not JSON */ }
const error = new Error(data?.error?.message ?? 'Request failed with status ' + res.status);
error.status = res.status;
error.code = data?.error?.code ?? 'HTTP_ERROR';
throw error;
}
// In a component:
// const { notes } = await api('/notes');
// const note = await api('/notes', { method: 'POST', body: { title } });Many teams use a small library such as ky or axios instead of writing this file. They do the same jobs: JSON, base URL and throwing on bad status codes. Writing it yourself once shows you what those libraries do.
Watch the lesson
Calling the API with fetch · 1/7
Our React screens now need real data from the Express API. The browser function for that is fetch.
Prefer a video by a teacher? Find free videos on “javascript fetch api post json error handling tutorial” on YouTube ↗
In real life: A food delivery app with one API client
A food delivery app has many screens: restaurants, menu, cart, payment and order tracking. All of them call the backend through one API client. When the team needed to send the app version with every request, so the server could tell old apps to update, they added one header in that single file. When the server started answering errors in a new shape, they changed one function, and every screen showed the right message at once.
Practice (0/3 done)
Write jsonOptions(method, data) that builds the options object for fetch. If data is undefined, return { method } only. Otherwise return { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }, with the keys in this order.
Write an async function createNote(fetchFn, input). It calls fetchFn("/api/notes", options) with method POST, the header Content-Type: application/json and body JSON.stringify(input). If res.ok is true, return the note from await res.json(). If not, read the error body (it looks like { error: { code, message } }) and throw new Error(message). fetchFn works like fetch; the tests pass a fake one.
Write the Notes app API client: async apiRequest(fetchFn, path, options = {}). Rules: 1) the URL is "/api" + path. 2) Always send credentials: "include"; the method is options.method or "GET". 3) Only if options.body is not undefined, add headers { "Content-Type": "application/json" } and body JSON.stringify(options.body). 4) If res.status is 204, return null without calling res.json(). 5) If res.ok, return await res.json(). 6) Otherwise throw an Error whose message is the server's error.message, or "Request failed with status N" when the body is not JSON or has no message; also set error.status to the status number.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.