Node.js Backend Fundamentals with Express · Build an API with Express

Your First Express API

Create an Express 5 app with JSON routes, start it, and call it, seeing exactly what the server sends back.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A small "books" API with GET /books, GET /books/:id and POST /books, built with Express 5 and Node.js 24. Every example in this course starts the app on a free port, sends real HTTP requests with fetch and prints the responses.

The idea

  • express() creates an app; app.get(path, handler) and app.post(...) register routes.
  • A handler receives the request (req.params, req.query, req.body) and the response (res.json(), res.status()).
  • express.json() is middleware that parses JSON request bodies into req.body.
  • app.listen(port) starts an HTTP server. In real apps the port comes from configuration (process.env.PORT).
  • Express 5 (the current major version) handles promises returned from async handlers: a thrown error reaches your error handler automatically.

The code

A JSON API with Express 5
import express from "express";

const books = [
  { id: 1, title: "Clean Code", author: "Robert C. Martin" },
  { id: 2, title: "Refactoring", author: "Martin Fowler" },
];

const app = express();
app.use(express.json());

app.get("/books", (req, res) => res.json(books));

app.get("/books/:id", (req, res) => {
  const book = books.find((b) => b.id === Number(req.params.id));
  if (!book) return res.status(404).json({ error: "book not found" });
  res.json(book);
});

app.post("/books", (req, res) => {
  const book = { id: books.length + 1, title: req.body.title, author: req.body.author };
  books.push(book);
  res.status(201).location(`/books/${book.id}`).json(book);
});

// start on a free port and call the API (in production: app.listen(process.env.PORT ?? 3000))
const server = app.listen(0);
const base = `http://127.0.0.1:${server.address().port}`;
const show = async (r) => console.log(r.status, JSON.stringify(await r.json()));

await show(await fetch(`${base}/books/2`));
await show(await fetch(`${base}/books/9`));
const created = await fetch(`${base}/books`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "The Pragmatic Programmer", author: "Hunt and Thomas" }),
});
console.log("Location:", created.headers.get("location"));
await show(created);
server.close();

Output

200 {"id":2,"title":"Refactoring","author":"Martin Fowler"}
404 {"error":"book not found"}
Location: /books/3
201 {"id":3,"title":"The Pragmatic Programmer","author":"Hunt and Thomas"}

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Forgetting app.use(express.json()), so req.body is undefined.
  • Comparing req.params.id (a string) with a number without converting it.
  • Returning 200 for everything; use 201 for created resources and 404 when something doesn't exist.
  • Calling res.json() twice in one handler (Cannot set headers after they are sent): return after responding early.

Key points

  • Routes map an HTTP method and path to a handler.
  • express.json() parses JSON bodies; res.status().json() sends responses.
  • Express 5 forwards errors from async handlers to error middleware.

Further reading: Express 5 routing · Migrating to Express 5

Practice questions

What does app.use(express.json()) do?

Answer

It registers middleware that parses requests with a JSON body and puts the result in req.body; without it req.body is undefined.

Why compare Number(req.params.id) rather than req.params.id with a numeric id?

Answer

Route parameters are always strings; comparing a string with a number using === is always false.

What changed for async handlers in Express 5?

Answer

Rejected promises and errors thrown in async handlers are passed to next(err) automatically, so they reach error-handling middleware without try/catch wrappers.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?