Routes and responses
Design route order
Arrange specific, parameterized, router, and fallback routes so matching is predictable.
Express checks routes in the order you register them and stops at the first match. There is no scoring, no “most specific wins”. Order is behavior, so we design it instead of discovering it in production.
The notes app has a page to create a note at /notes/new and a page to view one at /notes/:id. Both are GET requests. If /notes/:id comes first, a request for /notes/new matches it with id equal to "new", and the create form never renders.
Put literals before parameters
Create src/routes/notes.js with a router. Literal paths go first:
import { Router } from 'express'
export const notesRouter = Router()
notesRouter.get('/', (req, res) => {
res.send('all notes')
})
notesRouter.get('/new', (req, res) => {
res.send('new note form')
})
notesRouter.get('/:id', (req, res) => {
res.send(`note ${req.params.id}`)
})
Then mount it, and register the not-found handler after every other route:
import { notesRouter } from './routes/notes.js'
app.use('/notes', notesRouter)
app.use((req, res) => {
res.status(404).send('Not found')
})
The last app.use() has no path, so it matches everything. It only runs when every earlier route has declined, which is exactly what a fallback should do.
Write the table before the code
For any group of routes that could overlap, I write down the requests and the handler I expect. It takes a minute and it catches order bugs before they exist:
| Request | Expected handler |
|---|---|
GET /notes | list |
GET /notes/new | new form |
GET /notes/42 | show, id is "42" |
GET /notes/42/edit | 404 for now |
GET /assets/notes.css | static file |
GET /nothing | 404 |
Now the table becomes tests. This one fails until /new is registered before /:id:
test('the new form is not treated as an id', async () => {
const res = await request(app).get('/notes/new')
assert.equal(res.text, 'new note form')
})
Express 5 path rules
The path syntax got stricter in Express 5, and it will bite you if you copy older snippets. Three changes matter in practice.
Wildcards need a name. app.get('/files/*') is a hard error now. Write app.get('/files/*path') and read req.params.path.
Optional segments use braces instead of ?. /notes/:id? becomes /notes{/:id}.
Regular-expression characters are no longer allowed inside path strings. If you need a pattern, pass a real RegExp object as the path.
The upside is that a route now reads exactly like the URL it matches. I find that a fair trade.
Try this on your project: move /:id above /new, run the test, watch it fail, and move it back. Seeing the wrong handler answer once is worth more than any rule about ordering.
Lesson completed