Routes and responses

Send one clear response

Choose status, headers, content type, and body deliberately and return after completing the response.

A handler has exactly three ways to finish: send a response, call next(), or throw. Pick one per branch, and make the code return right after. Most double-response bugs come from a branch that sends and then keeps going.

Status, headers, body, in that order

Creating a note should say what happened and where the new thing lives:

notesRouter.post('/', express.json(), (req, res) => {
  const note = notes.create(req.body)
  res.status(201).location(`/notes/${note.id}`).json(note)
})

201 Created tells the client the request made something. The Location header says where. res.json() sets Content-Type: application/json and serializes the body. Three decisions, one line, all visible.

Deleting a note has nothing to return, so don’t invent a body:

notesRouter.delete('/:id', (req, res) => {
  notes.remove(req.params.id)
  res.status(204).end()
})

204 No Content with an empty body is the honest answer. A { "ok": true } body adds nothing the status doesn’t already say.

Return after you send

Here is a handler with a bug that the happy path never shows:

notesRouter.get('/:id', (req, res) => {
  const note = notes.find(req.params.id)
  if (!note) {
    res.status(404).json({ error: 'Note not found' })
  }
  res.json(note)
})

Ask for a note that exists and it works. Ask for a missing one and Node throws:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

The 404 went out, then res.json(null) tried to send a second response. The fix is one word:

  if (!note) {
    return res.status(404).json({ error: 'Note not found' })
  }

I write return res... on every early exit, even when nothing follows yet. Code gets added below later, and the return is already there.

Every branch terminates once

For a handler with several outcomes, I list them and check that each ends the request exactly one way:

  • valid input, note created: 201 with JSON body
  • invalid input: 400 with the validation errors
  • note not found: 404
  • unexpected failure: throw, and let the error middleware answer

The last one matters. When the database call rejects, don’t catch it and send a 500 yourself. Throw, or let the rejection propagate, and the error middleware we build later handles it in one place.

Express 5 tightened this

Two old shortcuts are gone. res.send(404) no longer sets a status, it would send the string. Use res.sendStatus(404) or res.status(404).send(...). And res.json(body, status) was removed in favor of chaining res.status(201).json(body). res.status() also now throws if you pass anything other than an integer between 100 and 999, so a typo like res.status('201') fails loudly.

Try this: write a supertest case for each branch above and assert on status, Content-Type, and body. When the four tests pass, you know each branch terminates once.

Lesson completed