Routes and responses
Read params, query, and body
Distinguish path identity, optional query controls, and parsed request bodies.
A request carries input in three places, and each one means something different. The path says which thing. The query string says how to look at it. The body carries the thing itself.
In the notes app: /notes/42 identifies note 42, ?q=express filters the list, and the body of a POST holds a new title. Let’s read each one.
Params identify
Route parameters live in req.params. Every value is a string, even when it looks like a number:
notesRouter.get('/:id', (req, res) => {
const id = Number(req.params.id)
if (!Number.isInteger(id)) {
return res.status(404).send('Not found')
}
res.send(`note ${id}`)
})
/notes/42 gives req.params.id === '42'. /notes/abc gives 'abc', and I treat that as a not-found, not a server error. There is no note whose id is abc.
Query controls
The query string lives in req.query. It is optional by nature: the route works without it, and it changes how the result looks:
notesRouter.get('/', (req, res) => {
const q = typeof req.query.q === 'string' ? req.query.q.trim() : ''
const page = Math.max(1, Number(req.query.page) || 1)
res.send(`search "${q}", page ${page}`)
})
Why the typeof check? Because ?q=a&q=b gives you an array, and q.trim() would throw. Never assume a query value is a single string.
Express 5 uses the simple query parser by default. ?filter[tag]=work is { 'filter[tag]': 'work' } now, not a nested object. If you relied on nesting in Express 4, that changed.
Body carries content
The body is the only place where you send the actual note. It also has no default parser:
notesRouter.post('/', express.json(), (req, res) => {
const { title } = req.body
res.status(201).send(`created "${title}"`)
})
In Express 5, req.body is undefined until a parser fills it. Express 4 gave you an empty object, which hid the missing parser. Now you get a clear TypeError: Cannot destructure property 'title' of 'req.body' as it is undefined the moment you forget one. That is a better failure.
One key, three places
The classic mistake is reading a value from wherever it happens to appear. Send the same key everywhere and watch which one wins:
curl -X POST 'http://localhost:3000/notes?title=from-query' \
-H 'Content-Type: application/json' \
-d '{"title":"from-body"}'
The response says created "from-body". The handler read req.body.title and nothing else, so the query string could not override it. That’s the contract: for every input, the route decides where it comes from, and reads only there.
Express 4 had req.param('title') that searched all three. Express 5 removed it, and I think that was the right call.
Try this: add a title route parameter too, /notes/:title, send all three, and confirm the handler still reads only the documented source.
Lesson completed