Express Routers & Route Organization
Reviewed & published by Brayan K
An Express Router is a mini-application of grouped routes that you mount under a shared URL prefix, letting you split a growing API into small, focused files.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll create routers with express.Router(), mount them with app.use('/api', router), read route params, chain methods with app.route(), split routes into files, and understand why route order matters.
What You'll Learn in This Lesson
- How to create a router with express.Router()
- Mounting a router under a prefix with app.use('/api', router)
- Reading route params from req.params (and query from req.query)
- Chaining methods on one path with app.route()
- Splitting routes into per-feature files for a clean structure
- Why route order decides which handler runs
1️⃣ Creating & Mounting a Router
Putting every route directly on app gets messy fast. express.Router() creates a mini-app: a self-contained group of routes you define with relative paths. You then mount it with app.use('/api/users', router), and every route inside it gains that prefix.
Route parameters — the :id in a path — arrive on req.params. Note they're always strings, so convert with Number() when you need a number.
import express from 'express';
import request from 'supertest'; // lets us call the app in-process
// A Router is a mini-app: a self-contained group of routes you can
// mount under a shared prefix. In a real project this lives in its
// own file, e.g. routes/users.js, and is exported.
const userRouter = express.Router();
userRouter.get('/', (req, res) => res.json({ users: ['ada', 'linus'] }));
userRouter.get('/:id', (req, res) => res.json({ id: req.params.id }));
const app = express();
// Mount it: every route inside now lives under /api/users
app.use('/api/users', userRouter);
const r1 = await request(app).get('/api/users');
console.log('GET /api/users ->', r1.status, JSON.stringify(r1.body));
const r2 = await request(app).get('/api/users/42');
console.log('GET /api/users/42 ->', r2.status, JSON.stringify(r2.body));2️⃣ Chaining Methods with app.route()
When several HTTP methods share the same path — say GET, PUT, and DELETE on /books/:id — repeating that string on every line is noisy. app.route(path) lets you declare the path once and chain the method handlers off it.
import express from 'express';
import request from 'supertest';
const app = express();
app.use(express.json());
const books = [{ id: 1, title: 'Node Basics' }];
// app.route() chains several HTTP methods onto ONE path so you don't
// repeat the string '/books/:id' on every line.
app.route('/books/:id')
.get((req, res) => {
const book = books.find(b => b.id === Number(req.params.id));
res.json(book || { error: 'not found' });
})
.delete((req, res) => res.json({ deleted: Number(req.params.id) }));
const g = await request(app).get('/books/1');
console.log('GET /books/1 ->', g.status, JSON.stringify(g.body));
const d = await request(app).delete('/books/1');
console.log('DELETE /books/1 ->', d.status, JSON.stringify(d.body));3️⃣ Route Order & Splitting Into Files
Express checks routes in the order you declare them and stops at the first match. That means a literal path like /users/me must come before a parameter path like /users/:id — otherwise :id matches first with id = "me" and your specific handler never runs.
import express from 'express';
import request from 'supertest';
const app = express();
// Express matches routes TOP TO BOTTOM and stops at the first hit.
// So a specific path ('/users/me') must be declared BEFORE a
// parameter path ('/users/:id'), or ':id' would swallow 'me'.
app.get('/users/me', (req, res) => res.json({ who: 'current user' }));
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }));
const me = await request(app).get('/users/me');
console.log('GET /users/me ->', JSON.stringify(me.body));
const id = await request(app).get('/users/7');
console.log('GET /users/7 ->', JSON.stringify(id.body));In real projects you put each feature's router in its own file and mount them all in app.js. Here's the standard structure (shown as code — it spans multiple files, so it isn't run on one page):
// routes/products.js ---------------------------------------
import { Router } from 'express';
const router = Router();
router.get('/', (req, res) => res.json(getAllProducts()));
router.get('/:id', (req, res) => res.json(getProduct(req.params.id)));
router.post('/', (req, res) => res.status(201).json(createProduct(req.body)));
export default router;
// app.js -----------------------------------------------------
import express from 'express';
import productRouter from './routes/products.js';
import userRouter from './routes/users.js';
const app = express();
app.use(express.json());
// Each feature gets its own prefix and its own file.
app.use('/api/products', productRouter);
app.use('/api/users', userRouter);
app.listen(3000, () => console.log('API on http://localhost:3000'));Your turn. Fill in the two blanks marked ___ to create and mount a router, then run it and compare with the expected output.
import express from 'express';
import request from 'supertest';
// 🎯 YOUR TURN — fill in the two ___ blanks, then run it.
// 1) Create a router.
const router = express.___(); // 👉 Router
router.get('/ping', (req, res) => res.json({ ok: true }));
const app = express();
// 2) Mount the router under the /api prefix.
app.use('___', router); // 👉 '/api'
const r = await request(app).get('/api/ping');
console.log(r.status, JSON.stringify(r.body));
// ✅ Expected output:
// 200 {"ok":true}Mini-Challenge: A Products Router
No blanks this time — build the router yourself from the brief. Add a list route, a by-id route with a 404 fallback, and mount it at /api/products. Run it and check your output against the example in the comments.
import express from 'express';
import request from 'supertest';
// 🎯 MINI-CHALLENGE: a products router
// 1. Build a Router with:
// GET '/' -> respond with the whole products array
// GET '/:id' -> find by Number(id); if missing, respond 404
// with { error: 'not found' }, else the product
// 2. Mount it at '/api/products'.
// 3. The three requests below should print 200, 200, and 404.
//
// ✅ Example output:
// list -> 200 [{"id":1,"name":"Pen"},{"id":2,"name":"Notebook"}]
// one -> 200 {"id":2,"name":"Notebook"}
// miss -> 404 {"error":"not found"}
const products = [{ id: 1, name: 'Pen' }, { id: 2, name: 'Notebook' }];
const productRouter = express.Router();
// your routes here...
const app = express();
// mount it here...
const all = await request(app).get('/api/products');
console.log('list ->', all.status, JSON.stringify(all.body));
const one = await request(app).get('/api/products/2');
console.log('one ->', one.status, JSON.stringify(one.body));
const miss = await request(app).get('/api/products/99');
console.log('miss ->', miss.status, JSON.stringify(miss.body));Common Errors (and the fix)
- ❌ Your router's routes return 404 — ✅ You forgot to mount it: add app.use('/api/users', router). Defining a router doesn't connect it to the app.
- ❌ A specific route never runs — ✅ A parameter route declared earlier is swallowing it. Move literal paths (/users/me) above parameter paths (/users/:id).
- ❌ req.body is undefined on POST — ✅ Add app.use(express.json()) before your routes so Express parses the JSON body.
- ❌ Math on req.params.id gives NaN or no match — ✅ Params are strings. Compare with Number(req.params.id) when your data uses numeric ids.
- ❌ Double prefix like /api/users/api/users — ✅ You wrote the full path inside the router too. Inside a mounted router, use relative paths ('/', '/:id').
Pro Tips
- 💡 One router per feature in a routes/ folder keeps files small and URLs changeable in one place.
- 💡 Group with app.route() when many methods share a path to cut repetition.
- 💡 Order from specific to general — both within a router and when mounting routers — so the right handler wins.
📋 Quick Reference — Express Routing
| Task | Syntax |
|---|---|
| Create a router | const r = express.Router() |
| Mount under a prefix | app.use('/api', r) |
| Read a route param | req.params.id |
| Read a query value | req.query.page |
| Chain methods on a path | app.route('/x').get(...).put(...) |
| Parse JSON bodies | app.use(express.json()) |
🎉 Lesson Complete!
- ✅ express.Router() groups related routes into a mountable mini-app
- ✅ app.use('/api/users', router) mounts a router under a prefix
- ✅ Route params live on req.params and arrive as strings
- ✅ app.route() chains multiple methods on one path
- ✅ Route order matters — declare specific paths before parameter paths
Practice quiz
What is an express.Router()?
- A database table
- A self-contained mini-app of routes you can mount under a prefix
- A middleware that parses JSON
- A logging tool
Answer: A self-contained mini-app of routes you can mount under a prefix. A Router groups related routes into a mountable mini-app with relative paths.
How do you mount a router so its routes live under /api/users?
- app.get('/api/users', router)
- app.mount('/api/users', router)
- router.use('/api/users', app)
- app.use('/api/users', router)
Answer: app.use('/api/users', router). app.use('/api/users', router) mounts the router; each inner route gains that prefix.
Inside a router mounted at /api/users, what path do you write for the by-id route?
- '/:id' (relative to the mount point)
- '/api/users/:id'
- '/users/:id'
- 'api/users/:id'
Answer: '/:id' (relative to the mount point). Router paths are relative to the mount point, so you write '/:id', not the full path.
Why does /users/:id declared before /users/me break the /users/me route?
- Parameter routes are disabled
- Express sorts routes alphabetically
- The :id route matches first with id = 'me', so /users/me never runs
- Routers cannot have literal paths
Answer: The :id route matches first with id = 'me', so /users/me never runs. Express stops at the first match, so the earlier :id route swallows 'me' as the id.
What does app.route('/books/:id') let you do?
- Mount another app
- Parse the request body
- Declare the path once and chain .get(), .delete(), etc.
- Redirect to a new URL
Answer: Declare the path once and chain .get(), .delete(), etc.. app.route(path) declares the path once and chains multiple method handlers off it.
Values in req.params arrive as what type?
- Numbers
- Strings
- Booleans
- Objects
Answer: Strings. Route params are always strings; convert with Number() when you need a number.
Which middleware must you add so req.body is populated on a JSON POST?
- express.static()
- express.urlencoded() only
- app.use(express.json())
- express.router()
Answer: app.use(express.json()). express.json() parses a JSON request body into req.body; without it req.body is undefined.
Your router's routes all return 404. What is the most likely cause?
- You forgot to mount it with app.use(prefix, router)
- You used app.route()
- The port is wrong
- You forgot express.json()
Answer: You forgot to mount it with app.use(prefix, router). Defining a router doesn't connect it; you must mount it with app.use before its routes respond.
What does a double prefix like /api/users/api/users usually mean?
- The port is duplicated
- express.json() is missing
- Two routers share a prefix
- You wrote the full path inside the mounted router instead of a relative one
Answer: You wrote the full path inside the mounted router instead of a relative one. Inside a mounted router use relative paths like '/' and '/:id', not the full prefixed path.
What is the recommended structure for a non-trivial Express API?
- One Router per feature, each in its own file, mounted in app.js
- All routes in a single giant function
- One file per route handler with no routers
- Define routes inside app.listen()
Answer: One Router per feature, each in its own file, mounted in app.js. Create one Router per feature in a routes/ folder and mount them all in app.js.
Continue this course
- Previous: Advanced Streams: pipe & pipeline
- Next: Request Validation (Zod & express-validator) — Validate and sanitize request input and return clean 400 errors
- Quick reference: Node.js cheat sheet › Express Routing & Middleware
Frequently asked questions
What is the difference between app.use and app.get with a Router?
app.get('/path', handler) registers a single GET route directly on the app. app.use('/prefix', router) MOUNTS a whole Router under a prefix — every route defined inside that router becomes prefix + its own path. So a router with router.get('/:id') mounted at app.use('/api/users', router) responds to GET /api/users/:id. Use app.use with a Router to group and prefix many related routes at once; use app.get for one-off endpoints.
Why does my /users/:id route match before /users/me?
Express evaluates routes in the order you declare them and stops at the first match. If app.get('/users/:id') is listed before app.get('/users/me'), then a request to /users/me matches the :id route first (with id = 'me') and your /users/me handler never runs. The fix is ordering: declare specific, literal routes BEFORE parameterized ones. As a rule, put the most specific paths at the top of the file.
How do I read route parameters and query strings?
Route parameters — the :id parts of a path — come from req.params (for example req.params.id). Query string values after the ? come from req.query (for example req.query.page for /search?page=2). The JSON body of a POST/PUT comes from req.body, but only after you add the express.json() middleware. All three are plain objects, and their values arrive as strings, so convert with Number() when you need a number.
Should I split my routes into separate files?
Yes, once you have more than a handful of endpoints. Create one Router per feature (users, products, orders), keep each in its own file under a routes/ folder, export the router, and mount them in app.js with app.use('/api/users', userRouter) and so on. This keeps each file small and focused, makes routes easy to find, and lets you change one feature's URLs in a single place. It's the standard structure for any non-trivial Express API.