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);// I6.1 · ~38 min · Intermediate
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
You will be able to
Warm-up · Activity 1 of 7
async function load() {
return 42;
}
const result = load();
console.log(result instanceof Promise, await result);Predict · Activity 2 of 7
try {
const response = await fetch(api + "/nope");
console.log("fulfilled", response.status, response.ok);
} catch (error) {
console.log("rejected", error.name);
}Practice · Activity 3 of 7
const books = await response.____();Practice · Activity 4 of 7
const response = await fetch(api + "/books");
console.log(response.status, response.headers.get("CONTENT-TYPE"));Practice · Activity 5 of 7
const response = await fetch(api + "/echo", {
method: "POST",
body: {title: "Dune"}
});
console.log(await response.text());Brain teaser · Activity 6 of 7
const response = await fetch(api + "/books");
console.log((await response.json()).length);
console.log((await response.json()).length);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
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.jsOutput
200 true application/json
[ 'Dune', 'Emma' ]
404 false Not found
201 { id: 3, title: 'Kindred', year: 1979 }
TypeError: fetch failedExercise 1 of 3
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.
Run the tests: response.json is not a function. What is response before you await it?
fetch returns a promise of a Response, and json() returns a promise of the data.
const response = await fetch(…); const books = await response.json();
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);
}
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 3
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.
Run the tests: book 9 gives a SyntaxError. What did the server send, and did fetch reject?
A 404 still fulfils the fetch promise. response.status tells you the code.
if (response.status === 404) return null; before you call json().
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();
}
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 3 of 3
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.
Run the tests: addBook gives {"error":"Send JSON"}. What does the server want to see?
Two things are missing: a headers option, and a body that is a JSON string.
headers: {"Content-Type": "application/json"}, body: JSON.stringify(book)
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();
}
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsimport {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 readWhy, 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);.
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 functionWhy, 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.
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 JSONWhy, 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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.