Understanding Router Integration
Imagine you're assembling a complex machine, where each component needs to be carefully connected to create a functioning whole. Connecting Express routers works in a similar way - we're taking our modular router components and integrating them into our main application in a way that creates a cohesive routing system.
When we connect routers to our Express application, we're essentially creating a routing hierarchy - a structured way for our application to handle different types of requests. Think of it like creating a building's directory system, where different departments handle specific types of inquiries.
Basic Router Integration
Let's start with the fundamental process of connecting routers to your Express application. We'll build this understanding step by step:
// First, let's look at our file structure
project/
├── app.js // Main application file
└── routes/
├── products.js // Product routes
├── users.js // User routes
└── orders.js // Order routes
// app.js - Basic Integration
const express = require('express');
const app = express();
// Import our router modules
const productsRouter = require('./routes/products');
const usersRouter = require('./routes/users');
const ordersRouter = require('./routes/orders');
// Here's where the connection happens
app.use(productsRouter);
app.use(usersRouter);
app.use(ordersRouter);
// Start the server
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
This basic integration works, but it's like having a building with no clear directory - visitors wouldn't know which department handles what. Let's improve this with path prefixes.
Using Path Prefixes
Adding path prefixes to our routers is like creating a clear directory system in our building. Each department gets its own designated area:
// app.js - With Path Prefixes
const express = require('express');
const app = express();
// Import routers
const productsRouter = require('./routes/products');
const usersRouter = require('./routes/users');
const ordersRouter = require('./routes/orders');
// Connect routers with clear prefixes
app.use('/api/products', productsRouter);
app.use('/api/users', usersRouter);
app.use('/api/orders', ordersRouter);
// This creates a clean API structure:
// - /api/products/* -> handled by productsRouter
// - /api/users/* -> handled by usersRouter
// - /api/orders/* -> handled by ordersRouter
Let's see how this affects our router files:
// routes/products.js - Before using prefixes
const express = require('express');
const router = express.Router();
// Notice how we needed the full path
router.get('/api/products', (req, res) => {
res.json({ message: 'Get all products' });
});
router.get('/api/products/:id', (req, res) => {
res.json({ message: `Get product ${req.params.id}` });
});
// routes/products.js - After using prefixes
const express = require('express');
const router = express.Router();
// Much cleaner! The prefix is handled in app.js
router.get('/', (req, res) => {
res.json({ message: 'Get all products' });
});
router.get('/:id', (req, res) => {
res.json({ message: `Get product ${req.params.id}` });
});
Advanced Router Mounting Techniques
Just as a large organization might have departments within departments, we can create more sophisticated routing hierarchies:
// routes/api/v1/products.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ version: 'v1', message: 'Get all products' });
});
module.exports = router;
// routes/api/v2/products.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ version: 'v2', message: 'Get all products' });
});
module.exports = router;
// app.js - API Versioning
const v1ProductsRouter = require('./routes/api/v1/products');
const v2ProductsRouter = require('./routes/api/v2/products');
// Mount different versions
app.use('/api/v1/products', v1ProductsRouter);
app.use('/api/v2/products', v2ProductsRouter);
Integrating Middleware with Mounted Routers
We can apply middleware at different levels of our routing hierarchy, just like having different security checkpoints in a building:
// Global middleware - applies to all routes
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
// API-level middleware - only for /api routes
app.use('/api', (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Authorization required' });
}
next();
});
// Router-specific middleware
const productsRouter = express.Router();
productsRouter.use((req, res, next) => {
console.log('Products router middleware');
next();
});
// Mount router with its middleware
app.use('/api/products', productsRouter);
Best Practices for Router Integration
Following these practices will help maintain a clean and maintainable routing system:
1. Centralized Route Management
// routes/index.js
const express = require('express');
const router = express.Router();
const productsRouter = require('./products');
const usersRouter = require('./users');
const ordersRouter = require('./orders');
// Configure all routes in one place
router.use('/products', productsRouter);
router.use('/users', usersRouter);
router.use('/orders', ordersRouter);
module.exports = router;
// app.js
const routes = require('./routes');
app.use('/api', routes);
2. Consistent Error Handling
// errorHandlers.js
const notFoundHandler = (req, res, next) => {
const error = new Error('Not Found');
error.status = 404;
next(error);
};
const errorHandler = (err, req, res, next) => {
res.status(err.status || 500);
res.json({
error: {
message: err.message,
status: err.status
}
});
};
// app.js
const { notFoundHandler, errorHandler } = require('./errorHandlers');
// Mount all routers first
app.use('/api', routes);
// Then add error handling
app.use(notFoundHandler);
app.use(errorHandler);
3. Route Organization by Feature
project/
├── app.js
└── routes/
├── products/
│ ├── index.js
│ ├── controllers.js
│ └── validation.js
├── users/
│ ├── index.js
│ ├── controllers.js
│ └── validation.js
└── index.js
Testing Connected Routers
Proper router integration makes testing easier and more organized:
// tests/integration/products.test.js
const request = require('supertest');
const express = require('express');
const productsRouter = require('../../routes/products');
describe('Products API', () => {
let app;
beforeEach(() => {
app = express();
app.use('/api/products', productsRouter);
});
it('should return all products', async () => {
const response = await request(app)
.get('/api/products')
.expect(200);
expect(response.body).toHaveProperty('products');
});
});