Node.js: Express Routing
Last updated: 2026-08-26
Charlie’s API project initially had only five routes, all written in app.js, which was still fairly clear. Two months later, the value of routes had grown to 30, and the file exceeded 500 lines—it took forever to make a single change to an API endpoint. The team decided to use express.Router() to split the routes into modules—user routes went into users.js, authentication routes into auth.js, and the custom logging middleware was moved to a separate file. After the split, each file had a single responsibility, and maintenance efficiency improved significantly.
You'll learn:
- Use
express.Router()to split modular routes - How to Read Route Parameters, Query Strings, and Request Bodies
- Writing Custom Middleware and the
next()Mechanism - Integration of third-party middleware (CORS / Morgan / Helmet)
- Design of Error-Handling Middleware and Global Error Handling
1. express.Router() Modular Routing
As the value of routes increases, putting all routes in app.js can result in bloated files and make collaboration difficult. express.Router() allows you to create independent route instances and then mount them to a specific path prefix in your application.
| Feature | Routes defined directly in the app | Modular routing via the Router module |
|---|---|---|
| File Organization | Everything in app.js | Split into separate files by module |
| Path Prefix | Specify the full path for each route | Set a uniform prefix when mounting |
| Team Collaboration | Multiple Users Editing the Same File | Independent Maintenance of Each Module |
| Reusability | Low | High; can be reused across projects |
▶ Example: Creating a Standalone User Routing Module
// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ users: ['Alice', 'Bob'] });
});
router.get('/:id', (req, res) => {
res.json({ user: req.params.id });
});
module.exports = router;
▶ Example: Mounting a route module in app.js
// app.js
const express = require('express');
const app = express();
const userRouter = require('./routes/users');
app.use('/users', userRouter);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Visiting /users matches router.get('/'), and visiting /users/42 matches router.get('/:id').
2. Route Parameters, Query Strings, and Request Bodies
Express offers three ways to receive client data, each suited to different scenarios.
| Data Source | How to Access | URL Example | Typical Uses |
|---|---|---|---|
| Routing Parameters | req.params |
/users/42 |
Identifies a specific resource |
| Query String | req.query |
/users?role=admin |
Filter, Sort, Search |
| Request Body | req.body |
POST body | Submit form/JSON data |
▶ Example: Route parameter :id
app.get('/products/:id', (req, res) => {
const productId = req.params.id;
res.json({ productId });
});
▶ Example: Query string req.query
app.get('/products', (req, res) => {
const { category, sort, page } = req.query;
res.json({ category, sort, page: page || 1 });
});
Access /products?category=electronics&sort=price&page=2, req.query, and { category: 'electronics', sort: 'price', page: '2' }.
▶ Example: Request body req.body and express.json()
app.use(express.json());
app.post('/products', (req, res) => {
const { name, price } = req.body;
res.status(201).json({ name, price });
});
Note: The values of
req.queryandreq.paramsare both strings and must be manually converted to numbers.
3. Middleware Basics and the next() Mechanism
Middleware is a core concept of Express—every request passes through a chain of middleware, and each middleware function can read the request, modify the response, or pass control to the next middleware function.
graph LR A[Request] --> B[morgan Log] B --> C[express.json Analysis] C --> D[Custom auth Middleware] D --> E[Route Handling Functions] E --> F[Response] D -->|next error| G[Error-handling middleware]
▶ Example: The Simplest Custom Middleware
function logger(req, res, next) {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next();
}
app.use(logger);
| Rule | Description |
|---|---|
next() must be called |
Otherwise, the request will be suspended, and the client will never receive a response |
| Call order is the execution order | Middleware registered first is executed first |
app.use() Applies globally |
app.use('/api', ...) Applies only to the /api path |
next('route') |
Skip the remaining middleware for the current route |
4. Integration with Third-Party Middleware
The community provides a wide range of ready-to-use middleware that can be enabled with a single line of code after installation.
| Middleware | Purpose | Installation Command |
|---|---|---|
| cors | Resolving Cross-Origin Request Issues | npm install cors |
| morgan | HTTP Request Logging | npm install morgan |
| helmet | Set up a safety response helmet | npm install helmet |
| express-rate-limit | Request Rate Limit | npm install express-rate-limit |
| cookie-parser | Parse Cookies | npm install cookie-parser |
▶ Example: Integrating CORS, Morgan, and Helmet
const cors = require('cors');
const morgan = require('morgan');
const helmet = require('helmet');
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
Note: It is recommended to place security-related middleware (Helmet) first, followed immediately by logging middleware.
5. Error-Handling Middleware
Express Convention: A function with four parameters (err, req, res, next) is an error-handling middleware. Whenever any middleware calls next(err), it skips the subsequent regular middleware and proceeds directly to the error-handling middleware.
| Comparison Item | Standard Middleware | Error-Handling Middleware |
|---|---|---|
| Number of parameters | 3 (req, res, next) | 4 (err, req, res, next) |
| Trigger Method | Execute sequentially when requests arrive | next(err) Trigger |
| Registration Location | Any Location | Must be placed last |
| Can there be more than one? | Yes | Yes, executed in order |
▶ Example: Global Error Handling Middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error'
});
});
▶ Example: Triggering an error in a route
app.get('/admin', (req, res, next) => {
const err = new Error('Access denied');
err.status = 403;
next(err);
});
6. Mermaid: Modular Routing Structure of the Router
graph TD APP[app.js] --> UR["/users → userRouter"] APP --> AR["/auth → authRouter"] APP --> MW["middleware/logger.js"] UR --> U1["GET / → User List"] UR --> U2["GET /:id → User Details"] UR --> U3["POST / → Create a User"] AR --> A1["POST /login → Log In"] AR --> A2["POST /register → Register"] MW --> ML["Logging Middleware"]
7. Comprehensive Example: Modular Routing API
In this example, the user routing, authentication routing, and logging middleware are split into separate files and ultimately integrated into app.js.
▶ Example: middleware/logger.js — Custom logging middleware
function logger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`);
});
next();
}
module.exports = logger;
▶ Example: routes/users.js — User Routes Module
const express = require('express');
const router = express.Router();
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
router.get('/', (req, res) => {
res.json(users);
});
router.get('/:id', (req, res, next) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
const err = new Error('User not found');
err.status = 404;
return next(err);
}
res.json(user);
});
router.post('/', (req, res) => {
const { name } = req.body;
if (!name) {
return res.status(400).json({ error: 'Name is required' });
}
const newUser = { id: users.length + 1, name };
users.push(newUser);
res.status(201).json(newUser);
});
module.exports = router;
▶ Example: routes/auth.js — Authentication Routes Module
const express = require('express');
const router = express.Router();
router.post('/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
res.json({ message: 'Login successful', token: 'mock-jwt-token' });
});
router.post('/register', (req, res) => {
const { username, email, password } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ error: 'All fields are required' });
}
res.status(201).json({ message: 'Registration successful' });
});
module.exports = router;
▶ Example: app.js — Integration Entry Point
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const logger = require('./middleware/logger');
const userRouter = require('./routes/users');
const authRouter = require('./routes/auth');
const app = express();
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(logger);
app.use('/users', userRouter);
app.use('/auth', authRouter);
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error'
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Run Test:
node app.js
Server running on port 3000
GET /users 200 3ms
GET /users/1 200 2ms
POST /auth/login 200 5ms
❓ FAQ
next() isn't called?next() in the middleware to pass control, or use methods like res.end() or res.json() to directly terminate the response.express.json() must be registered before any route that needs to read req.body; otherwise, req.body will be undefined.app.use() after all routes and middleware. Any middleware that calls next(err) will be routed to this handler.Access-Control-Allow-Origin to the response headers, allowing front-end applications from specified origins to access the API.express.Router() and using app.get() directly?Router creates a standalone router instance that can be mounted to any route prefix, making it suitable for modularization; app.get() registers directly on the application instance, making it suitable for simple projects. Both are functionally equivalent; the difference lies in how they are organized.req object in a middleware to pass data to subsequent processing?req, such as req.user = { id: 1 }. Subsequent middleware and routes can then access this data via req.user. This is a common pattern for communication between middleware in Express.📖 Summary
- 1 express.Router(): Core Concepts and Usage of Modular Routing
- 2 Core Concepts and Usage of Route Parameters, Query Strings, and Request Bodies
- 3 Core Concepts and Usage of Middleware Fundamentals and the next() Mechanism
- 4 Core Concepts and Usage Methods for Third-Party Middleware Integration
- 5 Core Concepts and Usage of Error-Handling Middleware
- 6 Mermaid: Core Concepts and Usage of the Modular Routing Structure in the Router Module
- 7 Comprehensive Example: Core Concepts and Usage of the Modular Routing API
📝 Exercises
- Complete all the code examples in this lesson and make sure each one runs correctly.
- Modify the comprehensive example and add your own extensions
- Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
- Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
- Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.