Express Route Groups
Introduction
As your Express application grows, managing routes can become increasingly complex. Route Groups provide a way to organize related routes together, making your codebase more maintainable and structured. This concept allows you to group routes that share common paths, middleware, or functionality under a single namespace.
Route groups are particularly helpful when:
- Your application has many endpoints
- You need to apply the same middleware to multiple routes
- You want to organize routes by feature or responsibility
- You're building a modular API
In this guide, we'll explore how to implement route groups in Express, best practices, and real-world examples.
Basic Route Grouping
The Express Router
The foundation of route grouping in Express is the Router
object. It allows you to create modular, mountable route handlers.
Let's start with a basic example:
const express = require('express');
const app = express();
const router = express.Router();
// Routes defined on the router
router.get('/', (req, res) => {
res.send('This is the home page of the group');
});
router.get('/about', (req, res) => {
res.send('About page of the group');
});
// Mount the router on a specific path
app.use('/group', router);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
In this example, the routes defined on the router will be accessible at:
http://localhost:3000/group/
- Home page of the grouphttp://localhost:3000/group/about
- About page of the group
Creating Multiple Route Groups
A real application typically has multiple logical groups of routes. Let's organize an API with different resource groups:
const express = require('express');
const app = express();
// User routes
const userRouter = express.Router();
userRouter.get('/', (req, res) => {
res.send('Get all users');
});
userRouter.get('/:id', (req, res) => {
res.send(`Get user with ID: ${req.params.id}`);
});
userRouter.post('/', (req, res) => {
res.send('Create a new user');
});
// Product routes
const productRouter = express.Router();
productRouter.get('/', (req, res) => {
res.send('Get all products');
});
productRouter.get('/:id', (req, res) => {
res.send(`Get product with ID: ${req.params.id}`);
});
productRouter.post('/', (req, res) => {
res.send('Create a new product');
});
// Mount routers
app.use('/api/users', userRouter);
app.use('/api/products', productRouter);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Now your API has the following endpoints:
/api/users/
- Get all users/api/users/:id
- Get a specific user/api/users/
(POST) - Create a user/api/products/
- Get all products/api/products/:id
- Get a specific product/api/products/
(POST) - Create a product
Route Group Middleware
One of the key benefits of route groups is the ability to apply middleware to all routes within a group. This is particularly useful for operations like:
- Authentication and authorization
- Request logging
- Request validation
- Error handling
Let's implement a simple authentication middleware for our user routes:
const express = require('express');
const app = express();
// Authentication middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized' });
}
// In a real app, you would validate the token here
const token = authHeader.split(' ')[1];
if (token !== 'valid-token') {
return res.status(401).json({ message: 'Invalid token' });
}
next(); // Proceed to the route handler
}
// User routes with authentication
const userRouter = express.Router();
// Apply middleware to all routes in this group
userRouter.use(authMiddleware);
userRouter.get('/', (req, res) => {
res.json({ users: ['John', 'Jane', 'Bob'] });
});
userRouter.get('/:id', (req, res) => {
res.json({ name: 'John Doe', id: req.params.id });
});
// Public routes without authentication
const publicRouter = express.Router();
publicRouter.get('/info', (req, res) => {
res.json({ message: 'This is public information' });
});
// Mount routers
app.use('/api/users', userRouter);
app.use('/public', publicRouter);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
In this example, all routes in the userRouter
require authentication, while routes in the publicRouter
do not.
Nested Route Groups
For more complex applications, you can nest routers to create a hierarchy of routes. This is useful for organizing routes by feature or module:
const express = require('express');
const app = express();
// Main API router
const apiRouter = express.Router();
// User routes
const userRouter = express.Router();
userRouter.get('/', (req, res) => {
res.send('Get all users');
});
userRouter.get('/:id', (req, res) => {
res.send(`Get user with ID: ${req.params.id}`);
});
// User posts sub-router
const userPostsRouter = express.Router({ mergeParams: true }); // Important for accessing parent params
userPostsRouter.get('/', (req, res) => {
res.send(`Get all posts for user ${req.params.userId}`);
});
userPostsRouter.get('/:postId', (req, res) => {
res.send(`Get post ${req.params.postId} for user ${req.params.userId}`);
});
// Mount nested routers
app.use('/api', apiRouter);
apiRouter.use('/users', userRouter);
apiRouter.use('/users/:userId/posts', userPostsRouter);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
This creates a hierarchical API with these endpoints:
/api/users/
- Get all users/api/users/:id
- Get a specific user/api/users/:userId/posts/
- Get all posts for a specific user/api/users/:userId/posts/:postId
- Get a specific post for a specific user
Note the { mergeParams: true }
option. This allows the nested router to access parameters from the parent router.
Modularizing Routes in Different Files
In a real-world application, you'll want to organize your routes in separate files. Here's how you can structure a modular Express application:
File Structure
/src
/routes
index.js # Main router assembly
users.js # User routes
products.js # Product routes
server.js # Main application file
server.js
const express = require('express');
const routes = require('./routes');
const app = express();
// Mount all routes from the routes module
app.use('/api', routes);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
routes/index.js
const express = require('express');
const userRoutes = require('./users');
const productRoutes = require('./products');
const router = express.Router();
router.use('/users', userRoutes);
router.use('/products', productRoutes);
module.exports = router;
routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ users: ['John', 'Jane', 'Bob'] });
});
router.get('/:id', (req, res) => {
res.json({ name: 'John Doe', id: req.params.id });
});
router.post('/', (req, res) => {
res.json({ message: 'User created successfully' });
});
module.exports = router;
routes/products.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ products: ['Laptop', 'Phone', 'Tablet'] });
});
router.get('/:id', (req, res) => {
res.json({ name: 'Laptop', id: req.params.id, price: 999 });
});
router.post('/', (req, res) => {
res.json({ message: 'Product created successfully' });
});
module.exports = router;
This structure allows you to:
- Keep your codebase organized
- Work on different route modules independently
- Reuse middleware across related routes
- Maintain clean separation of concerns
Real-World Example: E-commerce API
Let's create a more complete example of route groups for an e-commerce API:
const express = require('express');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Authentication middleware
function authenticate(req, res, next) {
// Implementation of authentication logic
console.log('User authenticated');
next();
}
// Admin authorization middleware
function authorizeAdmin(req, res, next) {
// Check if user is admin
console.log('Admin authorized');
next();
}
// API Router
const apiRouter = express.Router();
// Products Router
const productsRouter = express.Router();
productsRouter.get('/', (req, res) => {
res.json({ products: [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 699 },
{ id: 3, name: 'Tablet', price: 499 }
]});
});
productsRouter.get('/:id', (req, res) => {
res.json({ id: parseInt(req.params.id), name: 'Laptop', price: 999 });
});
// Orders Router - requires authentication
const ordersRouter = express.Router();
ordersRouter.use(authenticate); // All order routes require auth
ordersRouter.get('/', (req, res) => {
res.json({ orders: [
{ id: 101, products: [1, 2], total: 1698 },
{ id: 102, products: [3], total: 499 }
]});
});
ordersRouter.post('/', (req, res) => {
res.status(201).json({
message: 'Order created',
orderId: 103
});
});
// Admin Router - requires authentication and admin privileges
const adminRouter = express.Router();
adminRouter.use(authenticate);
adminRouter.use(authorizeAdmin);
adminRouter.get('/stats', (req, res) => {
res.json({
totalUsers: 103,
totalOrders: 215,
revenue: 157835
});
});
adminRouter.get('/users', (req, res) => {
res.json({ users: [
{ id: 1, name: 'Admin User', email: 'admin@example.com', role: 'admin' },
{ id: 2, name: 'Regular User', email: 'user@example.com', role: 'user' }
]});
});
// Mount routers
app.use('/api', apiRouter);
apiRouter.use('/products', productsRouter);
apiRouter.use('/orders', ordersRouter);
apiRouter.use('/admin', adminRouter);
app.listen(3000, () => {
console.log('E-commerce API running on port 3000');
});
This example demonstrates:
- Different levels of access control for different route groups
- Applying middleware to specific route groups
- A hierarchical organization of routes by function
- Clean separation between public, authenticated, and admin routes
Best Practices for Route Groups
When implementing route groups in Express, follow these best practices:
-
Organize by Feature: Group routes by the feature or resource they relate to, not by their HTTP method.
-
Keep it Shallow: Avoid deeply nesting routers as it can make your codebase harder to understand.
-
Consistent Naming: Use consistent naming conventions for both your route paths and router variables.
-
Apply Middleware Judiciously: Apply middleware at the appropriate level - don't add middleware to the entire app if it's only needed for specific routes.
-
Route Documentation: Include comments or use tools like Swagger to document your route groups and their endpoints.
-
Parameter Validation: Validate route parameters as early as possible in the request lifecycle.
-
Error Handling: Implement error handling for each route group to return appropriate status codes.
Summary
Express Route Groups, implemented using Express Router, provide a powerful way to organize your application's endpoints. They allow you to:
- Group related routes together
- Apply middleware to specific groups of routes
- Create modular, maintainable route structures
- Build hierarchical API paths
- Separate routes into different files
By properly implementing route groups, you can maintain a clean, organized codebase even as your application grows in complexity. This approach enhances maintainability and makes your Express application easier to develop and extend.
Additional Resources
Exercises
-
Create a route group for a blog API with endpoints for posts, comments, and categories.
-
Implement authentication middleware that protects only specific route groups in your application.
-
Refactor an existing Express application to use route groups organized in separate files.
-
Create a nested router structure for a social media API that includes users, posts, and comments.
-
Implement versioning for your API using route groups (e.g.,
/api/v1/users
and/api/v2/users
).
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)