Skip to main content

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:

javascript
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 group
  • http://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:

javascript
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:

javascript
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:

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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:

javascript
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:

  1. Organize by Feature: Group routes by the feature or resource they relate to, not by their HTTP method.

  2. Keep it Shallow: Avoid deeply nesting routers as it can make your codebase harder to understand.

  3. Consistent Naming: Use consistent naming conventions for both your route paths and router variables.

  4. 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.

  5. Route Documentation: Include comments or use tools like Swagger to document your route groups and their endpoints.

  6. Parameter Validation: Validate route parameters as early as possible in the request lifecycle.

  7. 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

  1. Create a route group for a blog API with endpoints for posts, comments, and categories.

  2. Implement authentication middleware that protects only specific route groups in your application.

  3. Refactor an existing Express application to use route groups organized in separate files.

  4. Create a nested router structure for a social media API that includes users, posts, and comments.

  5. 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! :)