Skip to content
aviral gupta

// I6.1 · ~38 min · Intermediate

fetch and JSON: loading and sending data

After this lesson you can load JSON from a server with fetch, tell a failed request from an error status, and send JSON with a POST request.

Lesson 1 of 5 in I6 Network requests and client storage

Start of the module

You will be able to

  • Load JSON with fetch: read status, ok and headers of the Response, then the body with json() or text()
  • Say when fetch rejects (no response at all) and when it fulfils with an error status such as 404
  • Send JSON with POST, a Content-Type header and JSON.stringify, and read each body only once
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson I1.3. What does this print?

    async function load() {
      return 42;
    }
    const result = load();
    console.log(result instanceof Promise, await result);
  2. Predict · Activity 2 of 7

    Predict before you read on. api is the address of a small test server, which has no page /nope. What does this print?

    try {
      const response = await fetch(api + "/nope");
      console.log("fulfilled", response.status, response.ok);
    } catch (error) {
      console.log("rejected", error.name);
    }
  3. Practice · Activity 3 of 7

    The server sends a JSON array of books. Fill in the method that reads the body and parses it as JSON.

    const books = await response.____();
    const books = await response.();
  4. Practice · Activity 4 of 7

    The server sends /books with the header Content-Type: application/json. What does this print?

    const response = await fetch(api + "/books");
    console.log(response.status, response.headers.get("CONTENT-TYPE"));
  5. Practice · Activity 5 of 7

    The test server’s /echo sends back exactly the body it got. What does this print?

    const response = await fetch(api + "/echo", {
      method: "POST",
      body: {title: "Dune"}
    });
    console.log(await response.text());
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The server sends two books. What does this print?

    const response = await fetch(api + "/books");
    console.log((await response.json()).length);
    console.log((await response.json()).length);
  7. Apply · Activity 7 of 7

    Mini-task: copy server.js from the worked example. Write addBook(api, book), which POSTs the book as JSON to /books and returns the book the server sends back (with its new id). Add {title: "Beloved", year: 1987}, print its id and title, then load /books and print how many books there are. Run node main.js.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Loading, missing and sending books

server.js is a tiny test server from Node.js’s standard library: it listens on a free port of your own computer and knows /books, /books/1 and /books/2. main.js starts it, loads the list, asks for a book that does not exist, sends a new one with POST, and finally stops the server and tries again.

main.js

import {startServer} from "./server.js";

const server = await startServer();
const api = server.url; // http://127.0.0.1 and a free port

const response = await fetch(api + "/books");
console.log(response.status, response.ok, response.headers.get("content-type"));
const books = await response.json(); // the body, parsed
console.log(books.map((book) => book.title));

const missing = await fetch(api + "/books/9"); // fulfils: a 404 is a response too
console.log(missing.status, missing.ok, await missing.text());

const created = await fetch(api + "/books", {
  method: "POST",
  headers: {"Content-Type": "application/json"},
  body: JSON.stringify({title: "Kindred", year: 1979})
});
console.log(created.status, await created.json());

await server.close();
try {
  await fetch(api + "/books"); // nobody answers any more
} catch (error) {
  console.log(error.name + ": " + error.message);
}

server.js

// A small test server for this lesson, made with node:http. You do not need to change it.
import {createServer} from "node:http";

export async function startServer() {
  const books = [
    {id: 1, title: "Dune", year: 1965},
    {id: 2, title: "Emma", year: 1815}
  ];
  const server = createServer(async (request, response) => {
    let body = "";
    for await (const chunk of request) body += chunk;
    const send = (status, type, text) => {
      response.writeHead(status, {"Content-Type": type});
      response.end(text);
    };
    const id = Number(request.url.split("/")[2]); // "/books/2" gives 2
    if (request.method === "GET" && request.url === "/books") {
      send(200, "application/json", JSON.stringify(books));
    } else if (request.method === "GET" && request.url.startsWith("/books/")) {
      const book = books.find((b) => b.id === id);
      if (book) send(200, "application/json", JSON.stringify(book));
      else send(404, "text/plain", "Not found");
    } else if (request.method === "POST" && request.url === "/books") {
      if (request.headers["content-type"] !== "application/json") {
        return send(415, "application/json", JSON.stringify({error: "Send JSON"}));
      }
      try {
        const book = {id: books.length + 1, ...JSON.parse(body)};
        books.push(book);
        send(201, "application/json", JSON.stringify(book));
      } catch {
        send(400, "application/json", JSON.stringify({error: "Bad JSON"}));
      }
    } else if (request.method === "POST" && request.url === "/echo") {
      send(200, "text/plain", body); // sends back exactly what it got
    } else {
      send(404, "text/plain", "Not found");
    }
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); // port 0: any free port
  return {
    url: `http://127.0.0.1:${server.address().port}`,
    close: () => new Promise((resolve) => server.close(resolve))
  };
}

Run it with

node main.js

Output

200 true application/json
[ 'Dune', 'Emma' ]
404 false Not found
201 { id: 3, title: 'Kindred', year: 1979 }
TypeError: fetch failed
  • Two awaits for each response: one for fetch, one for the body.
  • The missing book did not throw: fetch fulfilled with status 404, and ok was false.
  • Only when nothing answered at all did fetch reject, with TypeError: fetch failed.

Exercises

Exercise 1 of 3

The titles from the server

getTitles(baseUrl) should load baseUrl + "/books", which is a JSON array of books, and return an array of their titles. The starter forgets two awaits, so it calls json() on a promise instead of a Response.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Run the tests: response.json is not a function. What is response before you await it?

  2. Hint 2

    fetch returns a promise of a Response, and json() returns a promise of the data.

  3. Hint 3

    const response = await fetch(…); const books = await response.json();

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export async function getTitles(baseUrl) {
  const response = await fetch(baseUrl + "/books");
  const books = await response.json();
  return books.map((book) => book.title);
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export async function getTitles(baseUrl) {
  const response = fetch(baseUrl + "/books");
  const books = response.json();
  return books.map((book) => book.title);
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {startServer} from './server.js';
import {getTitles} from './main.js';

test('getTitles gives the titles of both books', async () => {
  const server = await startServer();
  try {
    const titles = await getTitles(server.url);
    assert.deepEqual(titles, ['Dune', 'Emma'], `getTitles gave ${JSON.stringify(titles)}`);
  } finally {
    await server.close();
  }
});

server.js

// A small test server for this lesson, made with node:http. You do not need to change it.
import {createServer} from "node:http";

export async function startServer() {
  const books = [
    {id: 1, title: "Dune", year: 1965},
    {id: 2, title: "Emma", year: 1815}
  ];
  const server = createServer(async (request, response) => {
    let body = "";
    for await (const chunk of request) body += chunk;
    const send = (status, type, text) => {
      response.writeHead(status, {"Content-Type": type});
      response.end(text);
    };
    const id = Number(request.url.split("/")[2]); // "/books/2" gives 2
    if (request.method === "GET" && request.url === "/books") {
      send(200, "application/json", JSON.stringify(books));
    } else if (request.method === "GET" && request.url.startsWith("/books/")) {
      const book = books.find((b) => b.id === id);
      if (book) send(200, "application/json", JSON.stringify(book));
      else send(404, "text/plain", "Not found");
    } else if (request.method === "POST" && request.url === "/books") {
      if (request.headers["content-type"] !== "application/json") {
        return send(415, "application/json", JSON.stringify({error: "Send JSON"}));
      }
      try {
        const book = {id: books.length + 1, ...JSON.parse(body)};
        books.push(book);
        send(201, "application/json", JSON.stringify(book));
      } catch {
        send(400, "application/json", JSON.stringify({error: "Bad JSON"}));
      }
    } else if (request.method === "POST" && request.url === "/echo") {
      send(200, "text/plain", body); // sends back exactly what it got
    } else {
      send(404, "text/plain", "Not found");
    }
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); // port 0: any free port
  return {
    url: `http://127.0.0.1:${server.address().port}`,
    close: () => new Promise((resolve) => server.close(resolve))
  };
}

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 3

A book, or null

findBook(baseUrl, id) loads baseUrl + "/books/" + id and returns the book. For a book that does not exist, the server answers 404 with the text Not found, and findBook must return null. The starter parses every answer as JSON, so a missing book throws a SyntaxError. Check the status first.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Run the tests: book 9 gives a SyntaxError. What did the server send, and did fetch reject?

  2. Hint 2

    A 404 still fulfils the fetch promise. response.status tells you the code.

  3. Hint 3

    if (response.status === 404) return null; before you call json().

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export async function findBook(baseUrl, id) {
  const response = await fetch(baseUrl + "/books/" + id);
  if (response.status === 404) return null;
  return response.json();
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export async function findBook(baseUrl, id) {
  const response = await fetch(baseUrl + "/books/" + id);
  return response.json();
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {startServer} from './server.js';
import {findBook} from './main.js';

test('findBook gives book 2', async () => {
  const server = await startServer();
  try {
    const book = await findBook(server.url, 2);
    assert.deepEqual(book, {id: 2, title: 'Emma', year: 1815}, `findBook gave ${JSON.stringify(book)}`);
  } finally {
    await server.close();
  }
});

test('findBook gives null for a book that does not exist', async () => {
  const server = await startServer();
  try {
    const book = await findBook(server.url, 9);
    assert.equal(book, null, `findBook gave ${JSON.stringify(book)}`);
  } finally {
    await server.close();
  }
});

server.js

// A small test server for this lesson, made with node:http. You do not need to change it.
import {createServer} from "node:http";

export async function startServer() {
  const books = [
    {id: 1, title: "Dune", year: 1965},
    {id: 2, title: "Emma", year: 1815}
  ];
  const server = createServer(async (request, response) => {
    let body = "";
    for await (const chunk of request) body += chunk;
    const send = (status, type, text) => {
      response.writeHead(status, {"Content-Type": type});
      response.end(text);
    };
    const id = Number(request.url.split("/")[2]); // "/books/2" gives 2
    if (request.method === "GET" && request.url === "/books") {
      send(200, "application/json", JSON.stringify(books));
    } else if (request.method === "GET" && request.url.startsWith("/books/")) {
      const book = books.find((b) => b.id === id);
      if (book) send(200, "application/json", JSON.stringify(book));
      else send(404, "text/plain", "Not found");
    } else if (request.method === "POST" && request.url === "/books") {
      if (request.headers["content-type"] !== "application/json") {
        return send(415, "application/json", JSON.stringify({error: "Send JSON"}));
      }
      try {
        const book = {id: books.length + 1, ...JSON.parse(body)};
        books.push(book);
        send(201, "application/json", JSON.stringify(book));
      } catch {
        send(400, "application/json", JSON.stringify({error: "Bad JSON"}));
      }
    } else if (request.method === "POST" && request.url === "/echo") {
      send(200, "text/plain", body); // sends back exactly what it got
    } else {
      send(404, "text/plain", "Not found");
    }
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); // port 0: any free port
  return {
    url: `http://127.0.0.1:${server.address().port}`,
    close: () => new Promise((resolve) => server.close(resolve))
  };
}

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 3 of 3

Sending a new book

addBook(baseUrl, book) should POST the book as JSON to baseUrl + "/books" and return what the server sends back: the book with its new id. The server only accepts a body with the header Content-Type: application/json. The starter sends the object itself, without the header, and gets an error object back.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Run the tests: addBook gives {"error":"Send JSON"}. What does the server want to see?

  2. Hint 2

    Two things are missing: a headers option, and a body that is a JSON string.

  3. Hint 3

    headers: {"Content-Type": "application/json"}, body: JSON.stringify(book)

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export async function addBook(baseUrl, book) {
  const response = await fetch(baseUrl + "/books", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify(book)
  });
  return response.json();
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export async function addBook(baseUrl, book) {
  const response = await fetch(baseUrl + "/books", {
    method: "POST",
    body: book
  });
  return response.json();
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {startServer} from './server.js';
import {addBook} from './main.js';

test('addBook gives the new book with id 3', async () => {
  const server = await startServer();
  try {
    const added = await addBook(server.url, {title: 'Kindred', year: 1979});
    assert.deepEqual(added, {id: 3, title: 'Kindred', year: 1979}, `addBook gave ${JSON.stringify(added)}`);
  } finally {
    await server.close();
  }
});

test('After addBook the server has three books', async () => {
  const server = await startServer();
  try {
    await addBook(server.url, {title: 'Kindred', year: 1979});
    const books = await (await fetch(server.url + '/books')).json();
    assert.equal(books.length, 3, `the server has ${books.length} books`);
  } finally {
    await server.close();
  }
});

server.js

// A small test server for this lesson, made with node:http. You do not need to change it.
import {createServer} from "node:http";

export async function startServer() {
  const books = [
    {id: 1, title: "Dune", year: 1965},
    {id: 2, title: "Emma", year: 1815}
  ];
  const server = createServer(async (request, response) => {
    let body = "";
    for await (const chunk of request) body += chunk;
    const send = (status, type, text) => {
      response.writeHead(status, {"Content-Type": type});
      response.end(text);
    };
    const id = Number(request.url.split("/")[2]); // "/books/2" gives 2
    if (request.method === "GET" && request.url === "/books") {
      send(200, "application/json", JSON.stringify(books));
    } else if (request.method === "GET" && request.url.startsWith("/books/")) {
      const book = books.find((b) => b.id === id);
      if (book) send(200, "application/json", JSON.stringify(book));
      else send(404, "text/plain", "Not found");
    } else if (request.method === "POST" && request.url === "/books") {
      if (request.headers["content-type"] !== "application/json") {
        return send(415, "application/json", JSON.stringify({error: "Send JSON"}));
      }
      try {
        const book = {id: books.length + 1, ...JSON.parse(body)};
        books.push(book);
        send(201, "application/json", JSON.stringify(book));
      } catch {
        send(400, "application/json", JSON.stringify({error: "Bad JSON"}));
      }
    } else if (request.method === "POST" && request.url === "/echo") {
      send(200, "text/plain", body); // sends back exactly what it got
    } else {
      send(404, "text/plain", "Not found");
    }
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); // port 0: any free port
  return {
    url: `http://127.0.0.1:${server.address().port}`,
    close: () => new Promise((resolve) => server.close(resolve))
  };
}

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Reading the body twice

import {startServer} from "./server.js";

const server = await startServer();
try {
  const response = await fetch(server.url + "/books");
  console.log("Books:", (await response.json()).length);
  const books = await response.json();
  console.log(books[0].title);
} finally {
  await server.close();
}

What Node.js prints

TypeError: Body is unusable: Body has already been read

Why, and the fix

The body is a stream, and json() reads it to the end. A second json(), text() or any other body method has nothing left to read. Read the body once, keep the result in a variable and use that: const books = await response.json(); console.log("Books:", books.length);.

No await before fetch

import {startServer} from "./server.js";

const server = await startServer();
try {
  const response = fetch(server.url + "/books");
  const books = await response.json();
  console.log(books.length);
} finally {
  await server.close();
}

What Node.js prints

TypeError: response.json is not a function

Why, and the fix

fetch returns a promise, not the Response. A promise has then and catch, but no json method. Await the fetch first: const response = await fetch(…);, then await the body as well.

Parsing an error page as JSON

import {startServer} from "./server.js";

const server = await startServer();
try {
  const response = await fetch(server.url + "/books/9");
  const book = await response.json();
  console.log(book.title);
} finally {
  await server.close();
}

What Node.js prints

SyntaxError: Unexpected token 'N', "Not found" is not valid JSON

Why, and the fix

There is no book 9, so the server answered 404 with the text Not found. fetch did not reject, because a 404 is a response. json() then tried to parse that text. Check the response first: if (!response.ok) { … } before you read the body as JSON.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

fetch gives you a Response

fetch(url) sends a GET request and returns a promise. It fulfils with a Response as soon as the status and the headers have arrived. response.status is the number, such as 200 or 404, response.ok is true for 200 to 299, and response.headers.get("content-type") reads a header, in any case. The body may still be on its way, so reading it is a second promise: await response.json() parses it as JSON, await response.text() gives the plain text. In Node.js 24, fetch is built in and works like in the browser.

A 404 is not a rejection

The fetch promise rejects only when no response arrives at all: the server cannot be reached, or the URL is broken. Then you get TypeError: fetch failed. A 404 or a 500 is still a response, so fetch fulfils, and only response.ok tells you that something went wrong. If you call json() on an error page, you get a SyntaxError about text that is not JSON. So check response.ok, or the status, before you use the body. Lesson I6.2 turns this check into errors and retries.

Sending JSON

To send data, pass options: method: "POST", headers: {"Content-Type": "application/json"} and body: JSON.stringify(data). Without JSON.stringify, the object is turned into the text "[object Object]". Without the header, fetch labels a string body as text/plain, and many servers refuse it. Without method, the request is a GET, which may not have a body at all. A body is a stream that can be read only once: a second json() throws TypeError: Body is unusable. Keep the parsed value in a variable instead.

Sources

Last reviewed October 5, 2026