Defining every route directly on app works for a handful of endpoints, but a real API quickly grows to dozens: products, orders, users, authentication, admin tools. One huge file full of app.get calls becomes hard to read, hard to test and easy to break.
Express solves this with routers. An express.Router() is a mini application that holds a group of related routes. You build a router for products in one file, a router for orders in another, and mount each one on a path prefix with app.use('/products', productsRouter). Inside the router, paths are written relative to that prefix, so the products file only needs to know about / and /:id.
Routers also support middleware, covered in the next lesson, so you can, for example, require a login for every route in the admin router with one line. In this lesson you'll create and mount routers, chain methods with router.route, work with several parameters and nested routes, load resources with router.param, and organise an Express project into folders.
Creating and mounting a router
// routes/products.js
import { Router } from 'express';
const router = Router();
router.get('/', (req, res) => res.json(products)); // GET /products
router.get('/:id', (req, res) => res.json(find(req.params.id))); // GET /products/7
router.post('/', (req, res) => res.status(201).json(create(req.body)));
export default router;// app.js
import express from 'express';
import productsRouter from './routes/products.js';
const app = express();
app.use(express.json());
app.use('/products', productsRouter);- Paths inside a router are relative to where it's mounted.
- The same router can be mounted in more than one place, for example under
/api/v1and/api/v2. - Routers are just middleware, so they run in the order they're added with
app.use.
router.route and multiple parameters
router.route(path) chains handlers for different methods on one path, so the path is written once:
router.route('/:id')
.get(getProduct)
.put(updateProduct)
.delete(deleteProduct);A path can have several parameters: /stores/:storeId/products/:productId gives req.params.storeId and req.params.productId.
For nested routers, such as reviews belonging to a product, create the child with Router({ mergeParams: true }) so it can see the parent's :id parameter.
router.param and Express 5 paths
router.param('id', handler) runs before any route that uses :id. It's a neat place to load the item once, return 404 if it doesn't exist, and attach it to req:
router.param('id', (req, res, next, id) => {
req.product = products.find((p) => p.id === Number(id));
if (!req.product) return res.status(404).json({ error: 'Not found' });
next();
});Express 5 uses a stricter, safer path syntax. Optional segments are written in braces, /files{/:name}, and wildcards need a name, /docs/*path. Old patterns such as /files/:name? and * from Express 4 tutorials no longer work.
Skipping handlers with next('route') and next('router')
next() moves to the next function in line, but two special values let you jump further.
next('route') skips the rest of the handlers on the current route and carries on with the next route that matches. It's useful when one path pattern covers two cases:
app.get('/items/:id', (req, res, next) => {
if (req.params.id === 'new') return next('route'); // let the next route handle it
res.send(`item ${req.params.id}`);
});
app.get('/items/:id', (req, res) => res.send('new item form'));A request for /items/5 gets "item 5", while /items/new falls through to the form.
next('router') leaves the whole router and returns to the app, which keeps looking for a match. A router mounted at / can use it to step aside: an admin router whose first middleware calls next('router') for non-admin users lets a public /stats route further down answer them instead.
Routers also take options. Router({ caseSensitive: true }) treats /Abc and /abc as different paths, and Router({ strict: true }) treats /abc/ and /abc as different. Both are off by default, which is what most APIs want. Finally, router.all(path, handler) matches every HTTP method, handy for a health check or for answering 405 Method Not Allowed after the real handlers.
Node.js Web Syntax
Router syntax
import { Router } from 'express';
const router = Router();
router.get('/', list);
router.route('/:id').get(show).put(update).delete(remove);
router.param('id', loadItem);
const reviews = Router({ mergeParams: true });
router.use('/:id/reviews', reviews);
export default router;
app.use('/api/products', router);
app.get('/files{/:name}', handler); // optional segment (Express 5)
app.get('/docs/*path', handler); // named wildcard (Express 5)Node.js Web Summary
- express.Router() groups related routes; app.use(prefix, router) mounts it.
- Paths inside a router are relative to its mount prefix.
- router.route(path) chains get, post, put and delete for one path.
- Nested routers need mergeParams: true to read parent parameters.
- router.param loads a resource for every route using that parameter.
Node.js Web Examples
Each example shows the code, the output it produces and what each part does.
Two routers mounted on prefixes
import express, { Router } from 'express';
const products = Router();
products.get('/', (req, res) => res.json(['Mug', 'Lamp']));
products.get('/:id', (req, res) => res.json({ product: Number(req.params.id) }));
const orders = Router();
orders.get('/', (req, res) => res.json({ orders: 2 }));
orders.post('/', (req, res) => res.status(201).json({ created: true }));
const app = express();
app.use('/api/products', products);
app.use('/api/orders', orders);
const server = app.listen(0, async () => {
const base = `http://localhost:${server.address().port}/api`;
const calls = [['GET', '/products'], ['GET', '/products/7'], ['GET', '/orders'], ['POST', '/orders'], ['GET', '/users']];
for (const [method, path] of calls) {
const r = await fetch(base + path, { method });
console.log(method, path, r.status, r.ok ? await r.text() : '');
}
server.close();
});GET /products 200 ["Mug","Lamp"]
GET /products/7 200 {"product":7}
GET /orders 200 {"orders":2}
POST /orders 201 {"created":true}
GET /users 404 Line-by-line explanation
- Lines 3 to 5The products router uses short paths relative to its prefix.
- Lines 7 to 9A second router handles orders independently.
- Lines 12 to 13Mounting adds the prefix, so / inside products becomes /api/products.
- Line 17No router handles /api/users, so Express returns 404.
router.route, router.param and nested routes
import express, { Router } from 'express';
const products = [{ id: 1, name: 'Mug', reviews: ['Great!'] }];
const router = Router();
router.param('id', (req, res, next, id) => {
req.product = products.find((p) => p.id === Number(id));
if (!req.product) return res.status(404).json({ error: `No product ${id}` });
next();
});
router.route('/:id')
.get((req, res) => res.json({ name: req.product.name }))
.delete((req, res) => res.sendStatus(204));
const reviews = Router({ mergeParams: true });
reviews.get('/', (req, res) => {
const product = products.find((p) => p.id === Number(req.params.id));
res.json({ product: req.params.id, reviews: product.reviews });
});
router.use('/:id/reviews', reviews);
const app = express();
app.use('/products', router);
const server = app.listen(0, async () => {
const base = `http://localhost:${server.address().port}/products`;
console.log(await (await fetch(`${base}/1`)).text());
console.log(await (await fetch(`${base}/9`)).text());
console.log((await fetch(`${base}/1`, { method: 'DELETE' })).status);
console.log(await (await fetch(`${base}/1/reviews`)).text());
server.close();
});{"name":"Mug"}
{"error":"No product 9"}
204
{"product":"1","reviews":["Great!"]}Line-by-line explanation
- Lines 6 to 10router.param loads the product once for every route with :id, or answers 404.
- Lines 12 to 14router.route writes the path once and chains a handler for each method.
- Line 16mergeParams lets the nested router read :id from the parent's path.
- Line 21The reviews router is mounted under each product's path.
Express 5 path patterns
import express from 'express';
const app = express();
app.get('/files{/:name}', (req, res) => {
res.send(req.params.name ? `File ${req.params.name}` : 'All files');
});
app.get('/docs/*path', (req, res) => {
res.send(`Doc path: ${req.params.path.join(' > ')}`);
});
app.get('/stores/:storeId/items/:itemId', (req, res) => {
const { storeId, itemId } = req.params;
res.send(`Store ${storeId}, item ${itemId}`);
});
const server = app.listen(0, async () => {
const base = `http://localhost:${server.address().port}`;
for (const p of ['/files', '/files/report.pdf', '/docs/guides/express/routing', '/stores/12/items/345']) {
console.log(await (await fetch(base + p)).text());
}
server.close();
});All files
File report.pdf
Doc path: guides > express > routing
Store 12, item 345Line-by-line explanation
- Line 5Braces make the /:name segment optional in Express 5.
- Line 9A named wildcard matches any number of segments.
- Line 10The wildcard's value is an array of the matched segments.
- Lines 13 to 14Several parameters can appear in one path and are destructured from req.params.
Common Node.js Web Mistakes
Repeating the prefix inside a router
router.get('/products/:id', show);
app.use('/products', router); // the route is now /products/products/:idrouter.get('/:id', show);
app.use('/products', router);Nested router without mergeParams
const reviews = Router();
reviews.get('/', (req, res) => res.json(req.params.id)); // undefinedconst reviews = Router({ mergeParams: true });
reviews.get('/', (req, res) => res.json(req.params.id));Express 4 wildcard syntax in Express 5
app.get('*', handler); // throws a path-to-regexp error in Express 5app.get('/*splat', handler); // or use app.use(handler) as a final catch-allNode.js Web in Real Projects
Routing in larger Express apps
Production APIs usually mount routers under a versioned prefix such as /api/v1, with one router per resource and an index.js that mounts them all. Admin routers get authentication middleware attached once at the router level. Frameworks like NestJS formalise the same idea with controllers, while simpler projects keep the folder-per-feature structure described in this lesson.
Node.js Web FAQ
How do I split Express routes into separate files?
Create a router in each file with const router = Router(), add its routes with paths relative to its prefix, and export default router. In your main app file, import each router and mount it with app.use('/products', productsRouter). Every route in the products file then lives under /products without repeating the prefix.
Why can't my nested router see the parent's route parameter?
By default, a router only sees the parameters in its own paths, so req.params.productId is undefined in a reviews router mounted at /products/:productId/reviews. Create the child with Router({ mergeParams: true }) and the parent's parameters are merged into req.params.
What is the difference between app.use and app.get for mounting routes?
app.use(path, handler) matches every HTTP method and any path that starts with the prefix, which is what you want for routers and middleware. app.get(path, handler) matches only GET (and HEAD) requests to that exact path pattern. Mount routers with app.use; define individual endpoints with get, post and the other method functions.
Why does Express match the wrong route for /products/new?
Express runs routes in the order you define them, so if /products/:id comes first, it happily matches new as an id. Put fixed paths such as /products/new or /products/search above the parameter route, or check the parameter in the handler and call next('route').
How do I version an Express API with routers?
Mount each version under its own prefix: app.use('/api/v1', v1Router) and app.use('/api/v2', v2Router). The two routers can share most of their route files and only differ where the API changed. Keeping old versions mounted lets existing clients carry on working while new ones move to v2.
Node.js Web Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the line to mount the router on /orders.
app.('/orders', ordersRouter);
What does express.Router() create?
A router defines router.get('/:id', ...) and is mounted with app.use('/api/users', router). Which URL matches?
What does router.route('/:id') let you do?
Why would you use router.param('id', ...)?
Node.js Web Quiz
5 questions to check what you learned.
Where do route parameters appear?
- req.query
- req.body
- req.route
- req.params
What option lets a nested router read its parent's parameters?
- strict: true
- mergeParams: true
- caseSensitive: true
- nested: true
How is an optional path segment written in Express 5?
- /files{/:name}
- /files/:name?
- /files/[name]
- /files/*name?
What's a good way to organise routes in a growing project?
- One file for every route
- Everything in app.js
- One router per feature, mounted under a prefix
- One router for GET and one for POST
In Express 5, what does a named wildcard like /docs/*path put in req.params.path?
- A string
- A number
- An array of the matched segments
- Nothing