InteractiveFrameworks

Routes and bodies

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'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)

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.