Routes and bodies — Basics · Express

Read request bodies with express.json(), create, change and delete cats, and declare the routes one path shares once with app.route(). Plus what Express 5 leaves in req.body when there is nothing to parse.

What you will learn

Read the theory for Routes and bodies

All Basics lessons

All Express courses

loading types…

What you'll learn

  • Mount express.json() so JSON request bodies arrive in req.body, and handle a body that was not parsed
  • Declare several methods on one path with app.route()
  • Answer a create with res.status(201).location(…).json(…) and a delete with res.sendStatus(204)

Routes and bodies

Reading data is half an API. The other half receives it: a new cat in a POST, a change in a PATCH, and those arrive in the request's body, which Express does not read on its own. This lesson adds the body parser, the routes that change the collection, and a tidier way to declare several methods on one path.

The body is not parsed until you ask

A request body arrives as a stream of bytes. Express leaves it alone unless a body parser reads it, and the one for JSON ships with Express:

app.use(express.json());

app.use() runs something for every request, before the routes that come after it; lesson 3 is about what else it can run. express.json() reads the body of any request whose Content-Type is application/json, parses it, and puts the result on req.body. There are siblings for other formats: express.urlencoded() for HTML forms, express.text() for plain text, express.raw() for bytes.

Two details decide whether a handler can trust req.body:

  • Order matters. The parser must be mounted before the routes that read the body. Mounted after them, it runs too late.
  • In Express 5, req.body is undefined when no parser read the body: no body, a Content-Type the parser does not handle, or no parser at all. Express 4 used to leave an empty object there. So const { name } = req.body throws on a request that sent text, and the client gets a 500 for its own mistake. Destructuring from req.body ?? {} makes a missing body look like an empty one, which the validation after it then refuses.

Changing the collection

The routes follow the HTTP course's rules, written in Express:

app.post('/owners', (req, res) => {
  const owner = { id: nextId(), ...req.body };
  owners.push(owner);
  res.status(201).location(`/owners/${owner.id}`).json(owner);
});

res.location(url) sets the Location header and, like status(), returns res, so the three calls chain. A delete that has nothing to say answers res.sendStatus(204), which sets the status and ends the response with no body; res.status(204).end() is the same. Validation belongs before the change: check what arrived, answer 400 and return if it is wrong, and only then store anything.

One path, several methods: app.route()

GET, PATCH and DELETE on /cats/:id share the path, and writing it three times invites a typo in one of them. app.route(path) names the path once and chains a handler per method:

app
  .route('/owners/:id')
  .get((req, res) => { /* read */ })
  .put((req, res) => { /* replace */ })
  .delete((req, res) => { /* remove */ });

Each handler is an ordinary route handler; only the declaration is shared. A method the route does not list falls through to Express's 404, as if the route did not exist.

Your task

  1. Parse JSON request bodies for every route.
  2. POST /cats creates a cat from the body's name (a string) and age (a number) under the next id, one more than the highest in use, and answers 201 with it and Location: /cats/<id>. Without both, including a request with no JSON body at all, it answers 400 with { "error": "name and age are required" }.
  3. Replace the GET /cats/:id route with app.route('/cats/:id'), declaring three handlers: GET answers the cat, PATCH applies the body's fields to it and answers it, DELETE removes it and answers 204 with no body. Each answers 404 with { "error": "Cat <id> not found" } for a cat that does not exist.

When it fails

  • Cannot destructure property 'name' of 'req.body' as it is undefined, and a 500: the parser is missing, mounted after the routes, or the request was not JSON. Mount express.json() first and destructure from req.body ?? {}.
  • "Create Milo" answers 200: res.json() alone keeps the default status. res.status(201) first.
  • "Delete Milo" answers 200 with OK or {}: res.send() or res.json() was used for the empty answer. res.sendStatus(204).

Remember

  • Nothing reads the body until a parser does: app.use(express.json()), before the routes.
  • In Express 5, an unparsed body is undefined; use req.body ?? {}.
  • res.status(201).location(url).json(value) for a create, res.sendStatus(204) for a delete.
  • app.route(path) declares a path once with a handler per method.
Stuck? Show a hint

app.use(express.json()) before the routes parses JSON bodies. In Express 5, req.body is undefined when no parser read the body, so destructure from req.body ?? {}. app.route('/cats/:id').get(…).patch(…).delete(…) chains the handlers. res.sendStatus(204) sends the status and nothing else.