Skip to content

Intermediate · Connecting frontend and backend · topic 17 of 34 · 30 min

Calling the API with fetch

Explanation

fetch is the browser function that sends HTTP requests from your React code to the Express API. To send data you give a method, a Content-Type header and a JSON.stringify body, then read the answer with res.json(). fetch does not fail on 404 or 500, so you must check res.ok yourself. Put all of this in one small API client that every screen uses.

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.

Think of it like this: Think of sending a letter by speed post. You write the address (the URL), tick what is inside on the form (the Content-Type header), pack the item (the JSON body) and get a reply. If the reply says "no such person at this address", the post office still did its job: a letter came back. fetch works the same way. A 404 answer is a delivered reply, not a failed delivery. Only when the letter cannot travel at all (no network) does fetch itself fail.
javascript
const res = await fetch('/api/notes');
console.log(res.status); // 200
const data = await res.json(); // { notes: [...] }
console.log(data.notes.length);
A GET request. res.json() reads the body and turns the JSON text into a JavaScript object. It also returns a Promise, so it needs await.

Sending JSON to the API

javascript
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
Creating a note. The body must be a string, so the object goes through JSON.stringify.
  • 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.

Statusres.okWhat the client should do
200 OKtrueRead the JSON with res.json()
201 CreatedtrueRead the new note from the body
204 No ContenttrueDo NOT call res.json(): there is no body
400 Bad RequestfalseRead { error } and show error.message next to the form
401 UnauthorizedfalseSend the user to the login page
404 Not FoundfalseShow "Note not found"
500 Server ErrorfalseShow a friendly "Please try again" message
Careful: Calling res.json() on a 204 answer, or on an HTML error page sent by a proxy, throws an error like "Unexpected end of JSON input". Check the status first, and wrap the error-body read in try/catch.

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.

javascript
// 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 } });
A complete API client. import.meta.env.VITE_API_URL is how Vite reads a setting from the .env file; only names starting with VITE_ reach the browser. credentials is explained in the next topic.
Tip: Write functions that receive fetch as a parameter (fetchFn) when you want to test them. A test can then pass a fake fetch that returns a ready answer, with no server and no network. All the coding tasks in this module work this way.

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

React --fetch--> Express API

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)

Task 1 · Code · Javascript

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.

Task 2 · Code · Javascript

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.

Task 3 · Code · Javascript

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

Question 1

1.The server answers 404 Not Found. What does await fetch(url) do?

Question 2

2.Your POST reaches Express but req.body is empty. What is the most likely cause?

Question 3

3.DELETE /api/notes/5 answers 204. What should the client do?

Question 4

4.Why build one API client used by every screen?

Question 5

5.What does body: JSON.stringify(note) do?

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.

← Previous: Pages and client-side routing