Files
What you'll learn
- Group routes in an express.Router() in their own file and mount it with app.use(path, router)
- Load what a route parameter names once with router.param, and scope middleware to one router
- Nest a router under a parameterised path with mergeParams, and read req.baseUrl, req.path and req.originalUrl
Routers
An app with twenty routes in one file is hard to read, and the routes that belong together, everything under /owners, sit next to everything else. Express's answer is the router: a mini application with its own routes and middleware, written in its own file and mounted under a path. It is the same idea as a NestJS controller with a path prefix, or a module that groups a feature, and it is how every Express app beyond a toy is organised.
A router is an app without listen()
// owners.ts
import { Router } from 'express';
export const ownersRouter = Router();
ownersRouter.get('/', (req, res) => { /* every owner */ });
ownersRouter.get('/:id', (req, res) => { /* one owner */ });
// main.ts
app.use('/owners', ownersRouter);
A router has get, post, use, route and the rest, exactly like app. Its paths are relative to where it is mounted: mounted at /owners, its / answers /owners and its /:id answers /owners/7. The router does not know its own prefix, which is what makes it reusable and lets the prefix live in one place.
Middleware for one router
router.use(fn) runs fn for requests that reach this router only. A check that applies to one resource, a header that says which part of the app answered, logging for one area: each goes on its router, and the rest of the app never runs it.
Where am I? baseUrl, path, originalUrl
Inside a mounted router, the request's URL is split in three:
| Property | For GET /owners/7?full=1 inside the router mounted at /owners |
|---|---|
req.baseUrl | /owners, where the router is mounted |
req.path | /7, the part the router matches against |
req.originalUrl | /owners/7?full=1, the URL as it arrived |
A log written inside a router uses originalUrl; path alone would drop the prefix.
Loading a parameter once: router.param()
Several routes on /:id all start by finding the thing the id names, and answering 404 if there is none. router.param(name, handler) does that once, for every route in the router whose path has :name, before the route runs:
ownersRouter.param('id', (req, res, next, id) => {
const owner = owners.find((o) => o.id === Number(id));
if (!owner) {
res.status(404).json({ error: `Owner ${id} not found` });
return;
}
res.locals.owner = owner;
next();
});
ownersRouter.get('/:id', (req, res) => {
res.json(res.locals.owner);
});
It is middleware with a fifth argument, the parameter's value: answer or call next(), as always.
Order inside a router
Routes are tried in the order they are declared, in a router as in the app. /:id matches any single segment, so a route for /stats declared after it is never reached: stats is taken for an id. Fixed paths go before parameterised ones.
Nested resources: mergeParams
An owner's cats live at /owners/:ownerId/cats. Mounting a router there reads naturally, ownersRouter.use('/:ownerId/cats', ownerCatsRouter), but by default a router only sees the parameters in its own paths, so req.params.ownerId is undefined inside it. Creating it with Router({ mergeParams: true }) merges in the parameters of the path it is mounted at.
Your task
data.ts holds the owners and the cats. Split the API into two routers:
cats.ts: every response carriesx-resource: cats.GET /answers every cat;GET /whereanswers{ baseUrl, path, originalUrl }as the request sees them; for routes with:id,router.paramloads the cat intores.locals.cator answers404with{ "error": "Cat <id> not found" }, andGET /:idanswersres.locals.cat.owners.ts: every response carriesx-resource: owners.GET /answers every owner;GET /:ownerIdanswers one or404with{ "error": "Owner <id> not found" }. A router of its own, mounted at/:ownerId/cats, answersGET /with that owner's cats, or the same 404.main.tsmounts them under/catsand/owners.
When it fails
- Every request is Express's 404 page: the routers are not mounted, or mounted at a path with a typo.
GET /cats/whereanswersCat where not found:/whereis declared after/:id.- "Ann's cats" is an empty list or a 404 for
undefined: the nested router cannot see:ownerId.Router({ mergeParams: true }). x-resourceis missing on one route: the router's middleware is declared after that route.
Remember
Router()groups routes and middleware;app.use(path, router)mounts it, and its paths are relative.- Inside a router,
req.baseUrlis the mount point,req.paththe rest,req.originalUrlthe whole URL. router.paramloads a parameter once for every route that has it.- A nested router needs
mergeParams: trueto read its parent's parameters.
Stuck? Show a hint
A router has the same methods as the app, and its paths are relative to where it is mounted: catsRouter.get('/') answers /cats. router.param('id', (req, res, next, id) => { … }) runs before any of that router's routes with :id. Router({ mergeParams: true }) makes the parent's params visible, so a router mounted at /:ownerId/cats can read req.params.ownerId. Declare /where before /:id.
Press Run tests to start the app. Its log appears here.Graded endpoints
Mounted under /cats: its GET / answers GET /cats, with the router's own header
router.param loaded the cat into res.locals before the route ran
The param handler answered, and the route never ran
Inside a router, req.baseUrl is where it is mounted and req.path is the rest; req.originalUrl is the whole URL
A second router, a second header
The owners router's own :ownerId route
404, with the owners router's header
The nested router read :ownerId from its mount path, which only mergeParams allows
The same router, another owner
There is no owner 9 to list cats for