Routing in Express maps an HTTP method and a path to a handler function: app.get('/users', handler) answers GET requests to /users, and app.post(), app.put() and app.delete() do the same for the other methods. A segment that starts with a colon, as in /users/:id, is a route parameter whose value lands in req.params. To keep a growing app in order, put related routes in an express.Router() and mount it under a prefix with app.use('/api', router).
GET /users/2 answers {"id":2,"name":"Linus"}. Route parameters are always strings, which is why the handler compares with Number(req.params.id). Send POST /users with the body {"name": "Grace"} to get 201, then DELETE /users/1 and GET /users to see the list change.
express.Router() with a prefix
A router is a mini app with its own routes and middleware. Its paths are relative: a route written as /:id inside a router mounted at /api/books answers /api/books/:id. In a real project each router lives in its own file and ends with module.exports = router.
GET /api/books/1 returns {"id":1,"title":"Dune"}. router.route('/:id') writes the path once and chains one handler per method; send PUT /api/books/1 and you get {"updated":"1"}. Change the mount path to /v2/books and the same router answers there instead.
Route order: the first match wins
Express tries routes in the order you register them and stops at the first one that sends a response. A parameter route catches anything in its position, so a fixed path registered after it is never reached:
GET /users/new answers {"route":"/users/:id","id":"new"}: the parameter route was registered first and took it. Move the /users/new route above /users/:id and run again to get {"route":"/users/new"}. Then send GET /books to reach the 404 handler at the bottom.
The 404 handler
Express answers an unmatched request with its own HTML page, Cannot GET /books. For an API you usually want JSON, so add an app.use() with no path as the very last route, as in the block above. It runs only when every route before it passed the request on. Errors thrown by handlers are a different case with their own four-argument handler, covered in Express error handling.
Route paths in Express 5
Express 5 changed the path syntax, and most snippets online are written for Express 4:
| Pattern | Express 4 | Express 5 |
|---|---|---|
| Any path | app.get('*', fn) | app.get('/*splat', fn) |
| Optional parameter | /users/:id? | /users{/:id} |
| Pattern on a parameter | /users/:id(\\d+) | Not supported; check the value in the handler |
A bare * or a ? after a parameter throws a TypeError when the route is registered, so the app does not start.
Common mistakes
Registering the 404 handler first. app.use() with no path matches everything, so every request gets the 404. Keep it at the bottom.
Forgetting the prefix is removed. Inside a router mounted at /api, req.path does not include /api. Use req.originalUrl when you need the full path.
Comparing a parameter with a number. req.params.id === 1 is always false because req.params.id is the string '1'.
Frequently Asked Questions
What is the difference between app.use and app.get?
app.get('/users', fn) runs fn only for GET requests whose path is exactly /users. app.use('/users', fn) runs for every method and for any path that starts with /users, which is why it is used to mount routers and middleware.
How do I make Express routes case sensitive?
Routes ignore case by default, so /Users matches /users. Turn that off with app.set('case sensitive routing', true), or per router with express.Router({ caseSensitive: true }).
Why is req.params empty inside a nested router?
A router only sees the parameters of its own paths. If it is mounted at /users/:userId/posts, create it with express.Router({ mergeParams: true }) so req.params.userId from the parent path is available.
Should I put every route in one file?
Not once you have more than a handful. Create one router per resource (routes/users.js, routes/orders.js), export it with module.exports = router, and mount each one in the main file with app.use().