Introduction to Express.js Architecture
Think of Express.js as a sophisticated restaurant kitchen. Just as a restaurant processes orders through various stations (prep, cooking, plating, etc.), Express.js processes HTTP requests through different layers of middleware before serving the final response. This system allows for modular, organized, and efficient handling of web requests.
Understanding Middleware
Middleware functions are like quality control stations in a manufacturing line. Each piece of middleware can:
- Inspect the incoming request (like a QA inspector checking raw materials)
- Modify the request or response objects (like a worker processing materials)
- End the request-response cycle (like a supervisor deciding if a product is ready)
- Call the next middleware in line (like passing work to the next station)
Basic Middleware Structure
// Basic middleware function
const simpleLogger = (req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
next(); // Pass control to next middleware
};
// Using the middleware
app.use(simpleLogger);
Middleware Execution Order
Middleware execution in Express follows a "waterfall" pattern, similar to water flowing through a series of filters. Here's a visual example:
app.use((req, res, next) => {
console.log('First middleware - Always runs');
next();
});
app.get('/api/*', (req, res, next) => {
console.log('API route middleware - Only runs for /api routes');
next();
});
app.get('/api/users', (req, res) => {
console.log('Route handler - Final destination');
res.send('User data');
});
Express Router vs Application
Think of an Express application as a large office building, and Express routers as different departments within that building. Each department (router) handles specific types of requests, making the overall system more organized and maintainable.
Creating a Router
// userRouter.js
const express = require('express');
const router = express.Router();
// Department-specific middleware
router.use((req, res, next) => {
console.log('User department middleware');
next();
});
// Handle user-related routes
router.get('/', (req, res) => {
res.send('List of users');
});
router.post('/', (req, res) => {
res.send('Create new user');
});
module.exports = router;
// app.js
const userRouter = require('./userRouter');
app.use('/users', userRouter);
RESTful Routing with Express Router
RESTful routing is like creating a standardized filing system for your web application. Here's a complete example of a product router implementing RESTful conventions:
// productRouter.js
const express = require('express');
const router = express.Router();
// Middleware specific to product routes
const validateProduct = (req, res, next) => {
if (!req.body.name || !req.body.price) {
return res.status(400).json({ error: 'Name and price required' });
}
next();
};
// GET /products - List all products
router.get('/', (req, res) => {
res.json([/* product list */]);
});
// GET /products/:id - Get single product
router.get('/:id', (req, res) => {
res.json({ id: req.params.id, name: 'Sample Product' });
});
// POST /products - Create product
router.post('/', validateProduct, (req, res) => {
res.status(201).json({ message: 'Product created' });
});
// PUT /products/:id - Update product
router.put('/:id', validateProduct, (req, res) => {
res.json({ message: 'Product updated' });
});
// DELETE /products/:id - Delete product
router.delete('/:id', (req, res) => {
res.json({ message: 'Product deleted' });
});
module.exports = router;
Error Handling Middleware
Error handling middleware functions like a safety net in a circus - they catch errors that occur anywhere in your application. They're distinguished by having four parameters instead of three:
// Regular middleware
app.use((req, res, next) => {
// Normal processing
next();
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
// Example of error throwing and handling
app.get('/risky-operation', (req, res, next) => {
try {
// Simulated error
throw new Error('Something went wrong');
} catch (err) {
next(err); // Pass error to error handling middleware
}
});
Real-World Application Examples
Authentication Middleware
const authenticateUser = (req, res, next) => {
const authToken = req.headers.authorization;
if (!authToken) {
return res.status(401).json({ error: 'No authentication token provided' });
}
try {
// Verify token (simplified example)
const user = verifyToken(authToken);
req.user = user; // Attach user to request
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Protected route
app.get('/dashboard', authenticateUser, (req, res) => {
res.json({ message: `Welcome ${req.user.name}` });
});
Request Rate Limiting
const rateLimit = () => {
const requestCounts = {};
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
requestCounts[ip] = requestCounts[ip] || [];
requestCounts[ip] = requestCounts[ip].filter(time => now - time < 60000);
if (requestCounts[ip].length >= 100) {
return res.status(429).json({ error: 'Too many requests' });
}
requestCounts[ip].push(now);
next();
};
};
app.use(rateLimit());
Best Practices and Common Patterns
When working with Express middleware and routing, follow these guidelines:
- Order your middleware carefully - more specific routes should come before general ones
- Use separate routers for different resource types to maintain clean code organization
- Always handle errors appropriately using error middleware
- Keep middleware functions focused and single-purpose
- Use meaningful names for your middleware functions and routers