Exploring ExpressJS Routing
Routing is the mechanism by which an application responds to client requests for specific endpoints. Express.js provides a powerful, flexible routing system that forms the backbone of every web application. Mastering Express routing is essential for building well-structured, maintainable APIs.
1. Getting Started with Express
Installation
npm install express
Basic Express Server
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
// Basic route
app.get('/', (req, res) => {
res.send('Hello, World!');
});
// Start server
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
2. HTTP Methods
Express supports all standard HTTP methods for RESTful API design.
| Method | Use Case | Express Method |
|---|---|---|
| GET | Retrieve data | app.get() |
| POST | Create new data | app.post() |
| PUT | Replace data (full update) | app.put() |
| PATCH | Partial update | app.patch() |
| DELETE | Delete data | app.delete() |
| OPTIONS | Get allowed methods | app.options() |
| HEAD | Get headers only | app.head() |
CRUD Operations Example
const express = require('express');
const app = express();
app.use(express.json()); // ✅ Parse JSON bodies
// In-memory data store (use database in production)
let users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
// ✅ GET all users
app.get('/api/users', (req, res) => {
res.json(users);
});
// ✅ GET single user by ID
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// ✅ POST — Create new user
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// ✅ PUT — Replace user (full update)
app.put('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const { name, email } = req.body;
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
user.name = name;
user.email = email;
res.json(user);
});
// ✅ PATCH — Partial update
app.patch('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
if (req.body.name) user.name = req.body.name;
if (req.body.email) user.email = req.body.email;
res.json(user);
});
// ✅ DELETE — Delete user
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
});
3. Route Parameters & Query Strings
Route Parameters
Route parameters are named segments in the URL path, denoted by :.
// Route parameter :id
app.get('/api/users/:id', (req, res) => {
const id = req.params.id;
res.json({ userId: id });
});
// Multiple parameters
app.get('/api/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
// Optional parameter (with ?)
app.get('/api/users/:id?', (req, res) => {
if (req.params.id) {
// Fetch specific user
} else {
// Fetch all users
}
});
Query Strings
Query strings are key-value pairs after the ? in the URL, accessible via req.query.
// URL: /api/users?page=2&limit=10&sort=asc
app.get('/api/users', (req, res) => {
const { page = 1, limit = 10, sort = 'asc' } = req.query;
res.json({
page: parseInt(page),
limit: parseInt(limit),
sort,
users: [] // Your data
});
});
// ✅ Production: Parse and validate query parameters
app.get('/api/products', (req, res) => {
const {
category,
minPrice,
maxPrice,
inStock = true,
page = 1,
limit = 20
} = req.query;
// Validate numeric parameters
const parsedPage = parseInt(page);
const parsedLimit = parseInt(limit);
const parsedMinPrice = parseFloat(minPrice);
const parsedMaxPrice = parseFloat(maxPrice);
if (isNaN(parsedPage) || parsedPage < 1) {
return res.status(400).json({ error: 'Invalid page parameter' });
}
if (isNaN(parsedLimit) || parsedLimit < 1 || parsedLimit > 100) {
return res.status(400).json({ error: 'Limit must be between 1 and 100' });
}
// Build filter object
const filter = {};
if (category) filter.category = category;
if (parsedMinPrice) filter.minPrice = parsedMinPrice;
if (parsedMaxPrice) filter.maxPrice = parsedMaxPrice;
filter.inStock = inStock === 'true';
// Fetch products with filter
// ...
res.json({
filters: filter,
pagination: { page: parsedPage, limit: parsedLimit }
});
});
4. Middleware — The Express Superpower
Middleware is a function that has access to the request, response, and the next middleware function in the application's request-response cycle.
What Middleware Can Do
- Execute any code
- Modify the request or response
- End the request-response cycle
- Call the next middleware in the stack
Types of Middleware
| Type | Description | Example |
|---|---|---|
| Application-level | Applied to the entire app | app.use(express.json()) |
| Router-level | Applied to a specific router | router.use(authMiddleware) |
| Error-handling | Handles errors (4 parameters) | app.use((err, req, res, next) => {}) |
| Built-in | Provided by Express | express.json(), express.static() |
Custom Middleware Examples
// ✅ 1. Logging middleware
const loggerMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
});
next();
};
app.use(loggerMiddleware);
// ✅ 2. Authentication middleware
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
// Verify token (JWT)
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Attach user to request
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
app.use('/api/admin', authMiddleware);
// ✅ 3. Rate limiting middleware
const rateLimitMap = new Map();
const RATE_LIMIT_WINDOW = 60000; // 1 minute
const MAX_REQUESTS = 100;
const rateLimitMiddleware = (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
const windowStart = now - RATE_LIMIT_WINDOW;
if (!rateLimitMap.has(ip)) {
rateLimitMap.set(ip, []);
}
const requests = rateLimitMap.get(ip).filter(time => time > windowStart);
if (requests.length >= MAX_REQUESTS) {
return res.status(429).json({
error: 'Too many requests. Please try again later.'
});
}
requests.push(now);
rateLimitMap.set(ip, requests);
next();
};
app.use(rateLimitMiddleware);
// ✅ 4. Error handling middleware (must be LAST)
const errorHandlerMiddleware = (err, req, res, next) => {
console.error('❌ Error:', err.stack);
const status = err.status || 500;
const message = err.message || 'Internal server error';
res.status(status).json({
error: message,
timestamp: new Date().toISOString(),
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
app.use(errorHandlerMiddleware);
5. Route Organization — Using Express.Router
For production applications, routes should be organized into separate modules using express.Router().
Project Structure
project/
├── src/
│ ├── routes/
│ │ ├── index.js # Main router
│ │ ├── users.js # User routes
│ │ ├── products.js # Product routes
│ │ └── auth.js # Auth routes
│ ├── controllers/
│ │ ├── userController.js
│ │ └── authController.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── validation.js
│ └── app.js # Main app
├── server.js # Entry point
└── package.json
Router Module Example
// routes/users.js
const express = require('express');
const router = express.Router();
const {
getUsers,
getUserById,
createUser,
updateUser,
deleteUser
} = require('../controllers/userController');
const { authMiddleware } = require('../middleware/auth');
const { validateUser } = require('../middleware/validation');
// Routes
router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', validateUser, createUser);
router.put('/:id', authMiddleware, validateUser, updateUser);
router.delete('/:id', authMiddleware, deleteUser);
module.exports = router;
Controller Example
// controllers/userController.js
const User = require('../models/User');
const getUsers = async (req, res, next) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
next(err);
}
};
const getUserById = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (err) {
next(err);
}
};
const createUser = async (req, res, next) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (err) {
next(err);
}
};
const updateUser = async (req, res, next) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (err) {
next(err);
}
};
const deleteUser = async (req, res, next) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(204).send();
} catch (err) {
next(err);
}
};
module.exports = {
getUsers,
getUserById,
createUser,
updateUser,
deleteUser
};
Main App — Using Routers
// app.js
const express = require('express');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
const authRoutes = require('./routes/auth');
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
app.use('/api/auth', authRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal server error'
});
});
module.exports = app;
6. Advanced Routing Patterns
Chainable Route Handlers
app.route('/api/users')
.get((req, res) => { /* GET users */ })
.post((req, res) => { /* POST user */ });
app.route('/api/users/:id')
.get((req, res) => { /* GET user */ })
.put((req, res) => { /* PUT user */ })
.delete((req, res) => { /* DELETE user */ });
Route Groups with Middleware
// Apply middleware to a group of routes
app.use('/api/admin', authMiddleware, (req, res, next) => {
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({ error: 'Admin access required' });
}
next();
});
// These routes all require authentication + admin access
app.get('/api/admin/users', adminController.getUsers);
app.delete('/api/admin/users/:id', adminController.deleteUser);
Parameter Validation Middleware
// middleware/validation.js
const { body, param, validationResult } = require('express-validator');
const validateUser = [
body('name').notEmpty().withMessage('Name is required'),
body('email').isEmail().withMessage('Valid email is required'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
const validateUserId = [
param('id').isInt().withMessage('ID must be an integer'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
module.exports = { validateUser, validateUserId };
7. Common Production Issues & Solutions
Issue 1: Routes Order Matters
// ❌ Wrong order — this catches everything!
app.get('/api/users/:id', (req, res) => { /* ... */ });
app.get('/api/users/profile', (req, res) => { /* ... */ });
// Request to /api/users/profile will match the :id route!
// ✅ Correct order — specific routes first
app.get('/api/users/profile', (req, res) => { /* ... */ });
app.get('/api/users/:id', (req, res) => { /* ... */ });
// Request to /api/users/profile matches the specific route first
Issue 2: Async Error Handling
// ❌ Unhandled promise rejection
app.get('/api/users', async (req, res) => {
const users = await User.find(); // If this fails, the app crashes
res.json(users);
});
// ✅ Wrap async handlers with error handler
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/api/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json(users);
}));
// ✅ Or use express-async-errors
require('express-async-errors');
app.get('/api/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
Issue 3: CORS Configuration
// ✅ Production CORS setup
const cors = require('cors');
const allowedOrigins = ['https://myapp.com', 'https://api.myapp.com'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 24 hours
}));
8. Production-Ready Checklist
- ✅ Use express.Router() — Organize routes into separate modules.
- ✅ Use controllers — Separate route definitions from business logic.
- ✅ Implement validation — Validate request data before processing.
- ✅ Handle errors — Use error-handling middleware.
- ✅ Use async/await — With proper error handling.
- ✅ Order routes correctly — Place specific routes before parameter routes.
- ✅ Add security middleware — Helmet.js, CORS, rate limiting.
- ✅ Log requests — Implement request logging middleware.
- ✅ Use environment variables — For configuration.
- ✅ Add health checks — Expose /health endpoint.
- ✅ Implement CORS — Properly configure for production.
- ✅ Add request validation — Use express-validator or Joi.
9. Quick Reference — Express Routing
| Concept | Syntax | Example |
|---|---|---|
| GET Route | app.get(path, handler) | app.get('/users', getUsers) |
| POST Route | app.post(path, handler) | app.post('/users', createUser) |
| Route Parameter | :param | app.get('/users/:id', getUser) |
| Query String | req.query | const { page } = req.query |
| Request Body | req.body | const { name } = req.body |
| Router | express.Router() | const router = express.Router() |
| Middleware | app.use(fn) | app.use(express.json()) |
| Error Handler | app.use((err, req, res, next) => {}) | 4 parameters required |
| Chained Routes | app.route(path).get().post() | app.route('/users').get(getUsers).post(createUser) |
Ready to master Node.js?
Build scalable, high-performance backend applications with Node.js. Learn from industry experts with hands-on projects.
.png)