Introduction to Routing
Routing is one of the most fundamental aspects of Express.js. It determines how an application responds to client requests to specific endpoints, which are defined by URIs (or paths) and HTTP methods (GET, POST, PUT, DELETE, etc.).
What is Routing?
Routing refers to how an application's endpoints (URIs) respond to client requests. Each route can have one or more handler functions, which are executed when the route is matched.
Basic Route Syntax
Express routes follow a simple pattern:
app.METHOD(PATH, HANDLER);
Where:
appis an instance of expressMETHODis an HTTP request method, in lowercase (get, post, put, delete, etc.)PATHis a path on the serverHANDLERis the function executed when the route is matched
Basic Routing Examples
const express = require('express');
const app = express();
// Respond with "Hello World!" for requests to the root URL (/)
app.get('/', (req, res) => {
res.send('Hello World!');
});
// Respond to POST request on the root route
app.post('/', (req, res) => {
res.send('Got a POST request');
});
// Respond to PUT request to /user route
app.put('/user', (req, res) => {
res.send('Got a PUT request at /user');
});
// Respond to DELETE request to /user route
app.delete('/user', (req, res) => {
res.send('Got a DELETE request at /user');
});
app.listen(3000, () => {
console.log('Example app listening on port 3000');
});
HTTP Methods and Their Common Uses
| Method | Description | Typical Use |
|---|---|---|
GET |
Request data from a resource | Retrieve data (read-only) |
POST |
Submit data to be processed | Create new resources |
PUT |
Update a resource | Replace an existing resource |
PATCH |
Partially update a resource | Modify parts of an existing resource |
DELETE |
Delete a resource | Remove resources |
OPTIONS |
Get allowed communication options | CORS preflight requests |
HEAD |
Same as GET but without response body | Check if resource exists without transferring data |
Route Parameters
Route parameters are named URL segments used to capture values at specific positions in the URL. The captured values are stored in the req.params object.
// Route with one parameter
app.get('/users/:userId', (req, res) => {
res.send(`User ID: ${req.params.userId}`);
});
// Route with multiple parameters
app.get('/users/:userId/books/:bookId', (req, res) => {
res.send(`User ID: ${req.params.userId}, Book ID: ${req.params.bookId}`);
});
// Example URLs and their params:
// GET /users/34 -> req.params.userId = "34"
// GET /users/45/books/123 -> req.params.userId = "45", req.params.bookId = "123"
Parameter Constraints with Regular Expressions
You can constrain route parameters to match specific patterns using regular expressions:
// Only match if userId is a 5-digit number
app.get('/users/:userId(\\d{5})', (req, res) => {
res.send(`User ID: ${req.params.userId}`);
});
// Only match if username contains lowercase letters and numbers only
app.get('/users/:username([a-z0-9]+)', (req, res) => {
res.send(`Username: ${req.params.username}`);
});
Real-World Example: Product Catalog API
// Route for product categories
app.get('/products/categories/:categoryName', (req, res) => {
const { categoryName } = req.params;
// Get products by category (mock implementation)
const products = getProductsByCategory(categoryName);
res.json(products);
});
// Route for specific product by ID (with validation)
app.get('/products/:productId(\\d+)', (req, res) => {
const productId = parseInt(req.params.productId);
// Get product details (mock implementation)
const product = getProductById(productId);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.json(product);
});
// Route for product reviews
app.get('/products/:productId/reviews', (req, res) => {
const { productId } = req.params;
// Get reviews for product (mock implementation)
const reviews = getProductReviews(productId);
res.json(reviews);
});
Query Parameters
Query parameters are key-value pairs in the URL after the question mark (?). They're typically used for filtering, sorting, pagination, or optional parameters.
// Route handling query parameters
app.get('/products', (req, res) => {
// Access query parameters
const { category, minPrice, maxPrice, sort } = req.query;
console.log('Query Parameters:');
console.log('Category:', category);
console.log('Min Price:', minPrice);
console.log('Max Price:', maxPrice);
console.log('Sort:', sort);
// Filter products based on query params (mock implementation)
let products = getAllProducts();
if (category) {
products = products.filter(p => p.category === category);
}
if (minPrice) {
products = products.filter(p => p.price >= parseFloat(minPrice));
}
if (maxPrice) {
products = products.filter(p => p.price <= parseFloat(maxPrice));
}
if (sort === 'price-asc') {
products.sort((a, b) => a.price - b.price);
} else if (sort === 'price-desc') {
products.sort((a, b) => b.price - a.price);
}
res.json(products);
});
// Example URLs:
// GET /products?category=electronics
// GET /products?minPrice=10&maxPrice=50
// GET /products?category=clothing&sort=price-asc
Route Parameters vs Query Parameters
| Route Parameters | Query Parameters |
|---|---|
| Part of the URL path | Appended after the ? in the URL |
| Typically required | Optional |
| Used for resource identification | Used for filtering, sorting, pagination |
Example: /users/:id |
Example: /users?role=admin&active=true |
Accessed via req.params |
Accessed via req.query |
Best Practices for Working with Query Parameters
- Set defaults for query parameters when they're not provided
- Validate and sanitize query parameter values (they are user input!)
- Convert types as needed (query parameters are always strings)
- Use clear, descriptive names for query parameters
- Document expected query parameters in your API documentation
// Example with proper validation and defaults
app.get('/api/search', (req, res) => {
// Set defaults and convert types
const query = req.query.q || '';
const page = parseInt(req.query.page || '1', 10);
const limit = parseInt(req.query.limit || '10', 10);
const sortBy = ['name', 'date', 'relevance'].includes(req.query.sort)
? req.query.sort
: 'relevance';
// Validate values
if (page < 1) {
return res.status(400).json({ message: 'Page must be greater than 0' });
}
if (limit < 1 || limit > 100) {
return res.status(400).json({ message: 'Limit must be between 1 and 100' });
}
if (query.length < 2) {
return res.status(400).json({ message: 'Search query must be at least 2 characters' });
}
// Calculate offset for pagination
const offset = (page - 1) * limit;
// Process search (mock implementation)
const results = searchItems(query, { offset, limit, sortBy });
const total = countSearchResults(query);
// Return results with pagination metadata
res.json({
results,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
});
Route Handlers
Route handlers are the functions that execute when a matching route is found. You can provide multiple handler functions that behave like middleware for a route.
Single Handler Function
app.get('/users', (req, res) => {
// Handle request for users list
res.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
});
Multiple Handler Functions
// Authentication middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ message: 'Authorization required' });
}
// Simplified auth check (for demonstration)
if (authHeader === 'Bearer valid-token') {
// Store user info in request object
req.user = { id: 123, role: 'admin' };
next(); // Pass control to the next handler
} else {
res.status(401).json({ message: 'Invalid token' });
}
}
// Permission check middleware
function checkAdmin(req, res, next) {
if (req.user && req.user.role === 'admin') {
next(); // User is admin, proceed to the next handler
} else {
res.status(403).json({ message: 'Admin access required' });
}
}
// Route with multiple handlers
app.get('/admin/settings', authenticate, checkAdmin, (req, res) => {
// This will only execute if both middleware functions pass
res.json({
message: 'Admin settings',
user: req.user
});
});
Handler Arrays
You can also provide an array of handler functions:
const validateUser = [
// Check if required fields exist
(req, res, next) => {
const { username, email, password } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ message: 'Missing required fields' });
}
next();
},
// Validate username format
(req, res, next) => {
const { username } = req.body;
if (!/^[a-zA-Z0-9_]{3,16}$/.test(username)) {
return res.status(400).json({
message: 'Username must be 3-16 characters and contain only letters, numbers, and underscores'
});
}
next();
},
// Validate email format
(req, res, next) => {
const { email } = req.body;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).json({ message: 'Invalid email format' });
}
next();
},
// Validate password strength
(req, res, next) => {
const { password } = req.body;
if (password.length < 8) {
return res.status(400).json({ message: 'Password must be at least 8 characters' });
}
next();
}
];
// Use the validation array in a route
app.post('/users', validateUser, (req, res) => {
// If we get here, all validations passed
const { username, email, password } = req.body;
// Create user (mock implementation)
const user = createUser({ username, email, password });
res.status(201).json({
message: 'User created successfully',
userId: user.id
});
});
Router Object
The Express Router object is a complete middleware and routing system that allows you to create modular, mountable route handlers. This is especially useful for organizing routes in larger applications.
Creating a Router Instance
const express = require('express');
const router = express.Router();
// Define routes on the router
router.get('/', (req, res) => {
res.send('Home page');
});
router.get('/about', (req, res) => {
res.send('About page');
});
// Export the router
module.exports = router;
Using the Router in Your Application
const express = require('express');
const app = express();
const mainRoutes = require('./routes/main');
// Mount the router on a specific path
app.use('/', mainRoutes);
app.listen(3000, () => {
console.log('Server started on port 3000');
});
Organizing Routes by Resource
A common pattern is to organize routes by resource type:
// routes/users.js
const express = require('express');
const router = express.Router();
// All routes here are prefixed with /users due to how it's mounted
router.get('/', (req, res) => {
res.send('List all users');
});
router.get('/:id', (req, res) => {
res.send(`Get user with ID: ${req.params.id}`);
});
router.post('/', (req, res) => {
res.send('Create new user');
});
router.put('/:id', (req, res) => {
res.send(`Update user with ID: ${req.params.id}`);
});
router.delete('/:id', (req, res) => {
res.send(`Delete user with ID: ${req.params.id}`);
});
module.exports = router;
// routes/products.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.send('List all products');
});
router.get('/:id', (req, res) => {
res.send(`Get product with ID: ${req.params.id}`);
});
// More product routes...
module.exports = router;
// app.js
const express = require('express');
const app = express();
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
// Mount routers on specific paths
app.use('/users', userRoutes);
app.use('/products', productRoutes);
app.listen(3000, () => {
console.log('Server started on port 3000');
});
Advanced Router Usage: API Versioning
One common application of the Router object is API versioning:
// routes/api/v1/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ version: 'v1', users: ['John', 'Jane'] });
});
module.exports = router;
// routes/api/v2/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
// V2 provides more detailed user objects
res.json({
version: 'v2',
users: [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
]
});
});
module.exports = router;
// app.js
const express = require('express');
const app = express();
// Import API route modules
const usersV1 = require('./routes/api/v1/users');
const usersV2 = require('./routes/api/v2/users');
// Mount API routes with versioning
app.use('/api/v1/users', usersV1);
app.use('/api/v2/users', usersV2);
// Redirect root API requests to latest version
app.get('/api', (req, res) => {
res.redirect('/api/v2');
});
app.listen(3000, () => {
console.log('Server started on port 3000');
});
Router-level Middleware
Just like with the main Express app, you can define middleware that applies only to a specific router:
// routes/admin.js
const express = require('express');
const router = express.Router();
// Middleware specific to admin routes
router.use((req, res, next) => {
// Check if user is admin
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ message: 'Admin access required' });
}
next();
});
// All routes below will only be accessible to admins
router.get('/dashboard', (req, res) => {
res.send('Admin Dashboard');
});
router.get('/reports', (req, res) => {
res.send('Admin Reports');
});
module.exports = router;
Introduction to Middleware
Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle.
What is Middleware?
Middleware functions can perform the following tasks:
- Execute any code
- Make changes to the request and response objects
- End the request-response cycle
- Call the next middleware function in the stack
The Assembly Line Analogy
Think of Express middleware as stations on an assembly line:
- The raw materials (incoming request) enter one end of the line
- Each station (middleware function) performs a specific operation
- Each station can modify the product as it passes through
- Each station can decide to pass the product to the next station or reject it
- At the end of the line, the finished product (response) is sent back
Just as a car might skip certain assembly stations based on its model, requests can follow different middleware paths based on their characteristics.
Middleware Function Signature
function myMiddleware(req, res, next) {
// Middleware logic here
console.log('Middleware executed!');
// If you want to pass control to the next middleware
next();
// Or if you want to end the request-response cycle
// res.send('Response from middleware');
}
Application-level Middleware
Application-level middleware is bound to an instance of the express application using app.use() or app.METHOD():
const express = require('express');
const app = express();
// Middleware without a mount path (applies to all routes)
app.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});
// Middleware with a mount path (applies only to specific paths)
app.use('/user/:id', (req, res, next) => {
console.log('Request Type:', req.method);
next();
});
// Middleware in a route handler
app.get('/user/:id', (req, res, next) => {
console.log('User ID:', req.params.id);
next();
}, (req, res) => {
res.send('User Info');
});
Router-level Middleware
Router-level middleware works in the same way as application-level middleware, except it is bound to an instance of express.Router():
const express = require('express');
const router = express.Router();
// Router-level middleware
router.use((req, res, next) => {
console.log('Router Time:', Date.now());
next();
});
router.get('/user/:id', (req, res) => {
res.send('User');
});
// Use the router in the app
app.use('/', router);
Built-in Middleware
Express comes with several built-in middleware functions that can be used to handle common tasks:
express.json()
Parses incoming requests with JSON payloads and makes the parsed data available on req.body.
// Parse JSON bodies
app.use(express.json());
// Now you can access JSON data in req.body
app.post('/api/users', (req, res) => {
console.log(req.body); // Contains parsed JSON data
res.json({ message: 'User created', data: req.body });
});
express.urlencoded()
Parses incoming requests with URL-encoded payloads (e.g., form submissions) and makes the parsed data available on req.body.
// Parse URL-encoded bodies (form data)
app.use(express.urlencoded({ extended: false }));
// Now you can access form data in req.body
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Process login
res.redirect('/dashboard');
});
The extended Option
The extended: true option allows for rich objects and arrays to be encoded into the URL-encoded format, allowing for a JSON-like experience with URL-encoded. When extended: false, the URL-encoded data is parsed with the Node.js querystring library (simpler, but more limited).
express.static()
Serves static files such as HTML, CSS, JavaScript, images, etc.
// Serve static files from the 'public' directory
app.use(express.static('public'));
// Serve static files with a virtual path prefix
app.use('/static', express.static('public'));
// Serve static files with an absolute path
const path = require('path');
app.use('/static', express.static(path.join(__dirname, 'public')));
Example: Serving Multiple Static Directories
const express = require('express');
const path = require('path');
const app = express();
// Serve static files from multiple directories
app.use(express.static(path.join(__dirname, 'public')));
app.use('/vendor', express.static(path.join(__dirname, 'node_modules')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Example of how files would be accessed:
// public/css/style.css -> http://localhost:3000/css/style.css
// node_modules/bootstrap/dist/css/bootstrap.min.css -> http://localhost:3000/vendor/bootstrap/dist/css/bootstrap.min.css
// uploads/profile.jpg -> http://localhost:3000/uploads/profile.jpg
Custom Middleware
Custom middleware allows you to add functionality specific to your application needs.
Logging Middleware
// Simple logging middleware
function requestLogger(req, res, next) {
const timestamp = new Date().toISOString();
const method = req.method;
const url = req.originalUrl || req.url;
const ip = req.ip || req.connection.remoteAddress;
console.log(`[${timestamp}] ${method} ${url} from ${ip}`);
// Pass control to the next middleware
next();
}
// Use the middleware
app.use(requestLogger);
Error Handling Middleware
Error handling middleware has a special signature with four arguments (err, req, res, next):
// Error handling middleware
function errorHandler(err, req, res, next) {
// Log the error
console.error(err.stack);
// Set appropriate status code
const statusCode = err.statusCode || 500;
// Send error response
res.status(statusCode).json({
status: 'error',
message: err.message || 'An unexpected error occurred',
// Only include error stack in development
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
// Use the error handler (should be defined last)
app.use(errorHandler);
// Example route that triggers an error
app.get('/error', (req, res, next) => {
// Create a custom error
const error = new Error('Sample error');
error.statusCode = 400;
// Pass the error to the error handler
next(error);
});
Authentication Middleware
// JWT authentication middleware
function authenticate(req, res, next) {
// Get the token from the Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Authentication required' });
}
const token = authHeader.split(' ')[1];
try {
// Verify the token (using a library like jsonwebtoken)
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add the user data to the request object
req.user = decoded;
// Proceed to the next middleware or route handler
next();
} catch (error) {
res.status(401).json({ message: 'Invalid token' });
}
}
// Use the middleware for specific routes
app.get('/api/profile', authenticate, (req, res) => {
res.json({
user: req.user,
profile: { /* user profile data */ }
});
});
Rate Limiting Middleware
// Simple in-memory rate limiter
function rateLimiter(options = {}) {
const { windowMs = 60000, maxRequests = 100, message = 'Too many requests' } = options;
const requests = new Map();
return (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
// Clean up old requests
if (requests.has(ip)) {
const userRequests = requests.get(ip).filter(time => now - time < windowMs);
requests.set(ip, userRequests);
if (userRequests.length >= maxRequests) {
return res.status(429).json({
message,
retryAfter: Math.ceil((windowMs - (now - userRequests[0])) / 1000)
});
}
} else {
requests.set(ip, []);
}
// Add current request timestamp
requests.get(ip).push(now);
next();
};
}
// Use the rate limiter
app.use('/api/', rateLimiter({
windowMs: 60 * 1000, // 1 minute
maxRequests: 100 // 100 requests per minute
}));
// For more sensitive routes, use a stricter limiter
app.use('/api/auth/', rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 5 // 5 requests per 15 minutes
}));
Note on Production Rate Limiting
For production applications, consider using established rate limiting middleware like express-rate-limit or rate-limiter-flexible instead of implementing your own. These packages provide more features, better performance, and are well-tested in production environments.
Third-Party Middleware
Express has a rich ecosystem of third-party middleware that you can use to add functionality to your application.
| Middleware | Purpose | Installation |
|---|---|---|
| morgan | HTTP request logger | npm install morgan |
| cors | Cross-Origin Resource Sharing | npm install cors |
| helmet | Security headers | npm install helmet |
| compression | Response compression | npm install compression |
| express-session | Session management | npm install express-session |
| cookie-parser | Parse cookie header | npm install cookie-parser |
| multer | File uploads | npm install multer |
Examples of Using Third-Party Middleware
Morgan (HTTP Request Logger)
const express = require('express');
const morgan = require('morgan');
const app = express();
// Use morgan middleware with predefined format
app.use(morgan('dev'));
// Or create a custom format
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
CORS (Cross-Origin Resource Sharing)
const express = require('express');
const cors = require('cors');
const app = express();
// Enable CORS for all routes
app.use(cors());
// Or configure CORS with options
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
// Or enable CORS for specific routes
app.get('/api/public', cors(), (req, res) => {
res.json({ message: 'This is public' });
});
Helmet (Security Headers)
const express = require('express');
const helmet = require('helmet');
const app = express();
// Use all default helmet middleware
app.use(helmet());
// Or configure specific middleware
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'trusted-cdn.com']
}
},
xssFilter: true,
noSniff: true
})
);
Express Session (Session Management)
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Using the session in routes
app.get('/login', (req, res) => {
// Set session data
req.session.user = { id: 1, username: 'john' };
res.redirect('/dashboard');
});
app.get('/dashboard', (req, res) => {
// Access session data
if (!req.session.user) {
return res.redirect('/login');
}
res.send(`Welcome, ${req.session.user.username}!`);
});
app.get('/logout', (req, res) => {
// Destroy session
req.session.destroy();
res.redirect('/');
});
Multer (File Uploads)
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
// Configure file filter
const fileFilter = (req, file, cb) => {
// Accept only images
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Not an image! Please upload an image.'), false);
}
};
// Create upload middleware
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 1024 * 1024 * 5 // 5MB limit
}
});
// Single file upload
app.post('/upload/profile', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ message: 'No file uploaded' });
}
res.json({
message: 'File uploaded successfully',
file: req.file
});
});
// Multiple files upload
app.post('/upload/gallery', upload.array('photos', 5), (req, res) => {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ message: 'No files uploaded' });
}
res.json({
message: `${req.files.length} files uploaded successfully`,
files: req.files
});
});
// Error handling for multer
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({
message: err.message
});
}
next(err);
});
Middleware Execution Order
The order in which middleware is defined is significant because middleware functions are executed sequentially.
const express = require('express');
const app = express();
// This middleware will execute first
app.use((req, res, next) => {
console.log('Middleware 1');
next();
});
// This middleware will execute second
app.use((req, res, next) => {
console.log('Middleware 2');
next();
});
// This middleware will execute third
app.use((req, res, next) => {
console.log('Middleware 3');
next();
});
// This route handler will execute last
app.get('/', (req, res) => {
console.log('Route handler');
res.send('Hello World');
});
// Output for a GET request to /:
// Middleware 1
// Middleware 2
// Middleware 3
// Route handler
Conditional Next
You can conditionally call next() to control the flow of execution:
app.use((req, res, next) => {
if (req.headers.authorization) {
// Process the request normally
next();
} else {
// Skip the next middleware and send a response
res.status(401).send('Authorization required');
}
});
Breaking the Chain
If you don't call next() or end the response (res.send(), res.json(), etc.), the request will hang:
// BAD EXAMPLE - request will hang
app.use((req, res, next) => {
if (someCondition) {
next();
}
// Missing else clause! Request will hang if condition is false
});
// GOOD EXAMPLE
app.use((req, res, next) => {
if (someCondition) {
next();
} else {
// Always end the response if not calling next()
res.status(400).send('Bad request');
}
});
Best Practices for Middleware Order
- Add logging middleware early to capture all requests
- Add security middleware (helmet, cors) early to protect all routes
- Add body parsing middleware (express.json, express.urlencoded) before routes that need parsed data
- Add session/cookie middleware before routes that need them
- Add authentication middleware before protected routes
- Add route-specific middleware directly to the routes that need them
- Add error handling middleware last to catch any errors
// Example of effective middleware ordering
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const session = require('express-session');
const app = express();
// Logging middleware (first to capture all requests)
app.use(morgan('dev'));
// Security middleware
app.use(helmet());
app.use(cors());
// Request parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session middleware
app.use(session({ /* config */ }));
// Serving static files
app.use(express.static('public'));
// Application routes
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
// 404 handler for unmatched routes
app.use((req, res, next) => {
res.status(404).json({ message: 'Route not found' });
});
// Error handling middleware (last)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: 'Internal server error' });
});
Practical Exercise
Exercise: Build a Blog API with Express Router and Custom Middleware
In this exercise, you'll build a simple blog API using Express routers and custom middleware.
Project Structure
blog-api/
├── app.js
├── middleware/
│ ├── logger.js
│ ├── auth.js
│ └── errorHandler.js
├── routes/
│ ├── posts.js
│ └── comments.js
└── data/
├── posts.js
└── comments.js
Step 1: Initialize the Project
mkdir blog-api
cd blog-api
npm init -y
npm install express uuid
Step 2: Create the Data Files
Create a data directory and add the following files:
data/posts.js:
// Initial sample data
let posts = [
{
id: '1',
title: 'Introduction to Express',
content: 'Express is a minimal and flexible Node.js web application framework...',
author: 'John Doe',
timestamp: '2025-05-01T10:00:00.000Z'
},
{
id: '2',
title: 'Working with Middleware',
content: 'Middleware functions are functions that have access to the request object...',
author: 'Jane Smith',
timestamp: '2025-05-02T14:30:00.000Z'
}
];
module.exports = {
getAll: () => posts,
getById: (id) => posts.find(post => post.id === id),
create: (post) => {
posts.push(post);
return post;
},
update: (id, updatedPost) => {
const index = posts.findIndex(post => post.id === id);
if (index === -1) return null;
posts[index] = { ...posts[index], ...updatedPost };
return posts[index];
},
delete: (id) => {
const index = posts.findIndex(post => post.id === id);
if (index === -1) return false;
posts.splice(index, 1);
return true;
}
};
data/comments.js:
// Initial sample data
let comments = [
{
id: '1',
postId: '1',
content: 'Great introduction to Express!',
author: 'Alice',
timestamp: '2025-05-01T11:30:00.000Z'
},
{
id: '2',
postId: '1',
content: 'Very helpful article.',
author: 'Bob',
timestamp: '2025-05-01T12:15:00.000Z'
},
{
id: '3',
postId: '2',
content: 'Middleware is so powerful!',
author: 'Charlie',
timestamp: '2025-05-02T15:45:00.000Z'
}
];
module.exports = {
getAll: () => comments,
getByPostId: (postId) => comments.filter(comment => comment.postId === postId),
create: (comment) => {
comments.push(comment);
return comment;
},
update: (id, updatedComment) => {
const index = comments.findIndex(comment => comment.id === id);
if (index === -1) return null;
comments[index] = { ...comments[index], ...updatedComment };
return comments[index];
},
delete: (id) => {
const index = comments.findIndex(comment => comment.id === id);
if (index === -1) return false;
comments.splice(index, 1);
return true;
}
};
Step 3: Create the Middleware Functions
Create a middleware directory and add the following files:
middleware/logger.js:
// Logger middleware
function logger(req, res, next) {
const timestamp = new Date().toISOString();
const method = req.method;
const url = req.originalUrl || req.url;
console.log(`[${timestamp}] ${method} ${url}`);
// Add request timestamp to the request object
req.requestTime = timestamp;
next();
}
module.exports = logger;
middleware/auth.js:
// Simulated authentication middleware
// In a real application, this would verify tokens, session cookies, etc.
function authenticate(req, res, next) {
// For demonstration, we'll use a simple API key check
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== 'secret-api-key') {
return res.status(401).json({ message: 'Authentication required' });
}
// Set user info (in a real app, this would come from a token or database)
req.user = {
id: '123',
name: 'Admin User',
role: 'admin'
};
next();
}
// Optional middleware for specific roles
function requireRole(role) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: 'Authentication required' });
}
if (req.user.role !== role) {
return res.status(403).json({ message: `Role '${role}' required for this action` });
}
next();
};
}
module.exports = {
authenticate,
requireRole
};
middleware/errorHandler.js:
// Error handling middleware
function errorHandler(err, req, res, next) {
// Log the error
console.error(`Error: ${err.message}`);
console.error(err.stack);
// Determine status code
const statusCode = err.statusCode || 500;
// Send error response
res.status(statusCode).json({
status: 'error',
message: err.message || 'Internal Server Error'
});
}
// 404 handler middleware
function notFoundHandler(req, res, next) {
const error = new Error(`Not Found - ${req.originalUrl}`);
error.statusCode = 404;
next(error);
}
module.exports = {
errorHandler,
notFoundHandler
};
Step 4: Create the Route Files
Create a routes directory and add the following files:
routes/posts.js:
const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const posts = require('../data/posts');
const { authenticate, requireRole } = require('../middleware/auth');
// GET all posts
router.get('/', (req, res) => {
res.json(posts.getAll());
});
// GET a specific post
router.get('/:id', (req, res) => {
const post = posts.getById(req.params.id);
if (!post) {
return res.status(404).json({ message: 'Post not found' });
}
res.json(post);
});
// POST a new post (requires authentication)
router.post('/', authenticate, (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({ message: 'Title and content are required' });
}
const newPost = {
id: uuidv4(),
title,
content,
author: req.user.name,
timestamp: new Date().toISOString()
};
posts.create(newPost);
res.status(201).json(newPost);
});
// PUT (update) a post (requires authentication)
router.put('/:id', authenticate, (req, res) => {
const { title, content } = req.body;
const post = posts.getById(req.params.id);
if (!post) {
return res.status(404).json({ message: 'Post not found' });
}
// Only allow updates to own posts or if admin
if (post.author !== req.user.name && req.user.role !== 'admin') {
return res.status(403).json({ message: 'You can only update your own posts' });
}
const updatedPost = posts.update(req.params.id, { title, content });
res.json(updatedPost);
});
// DELETE a post (requires admin role)
router.delete('/:id', authenticate, requireRole('admin'), (req, res) => {
const success = posts.delete(req.params.id);
if (!success) {
return res.status(404).json({ message: 'Post not found' });
}
res.status(204).end();
});
module.exports = router;
routes/comments.js:
const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const comments = require('../data/comments');
const posts = require('../data/posts');
const { authenticate } = require('../middleware/auth');
// GET all comments for a post
router.get('/post/:postId', (req, res) => {
const postId = req.params.postId;
// Check if post exists
const post = posts.getById(postId);
if (!post) {
return res.status(404).json({ message: 'Post not found' });
}
const postComments = comments.getByPostId(postId);
res.json(postComments);
});
// POST a new comment (requires authentication)
router.post('/', authenticate, (req, res) => {
const { postId, content } = req.body;
if (!postId || !content) {
return res.status(400).json({ message: 'Post ID and content are required' });
}
// Check if post exists
const post = posts.getById(postId);
if (!post) {
return res.status(404).json({ message: 'Post not found' });
}
const newComment = {
id: uuidv4(),
postId,
content,
author: req.user.name,
timestamp: new Date().toISOString()
};
comments.create(newComment);
res.status(201).json(newComment);
});
// PUT (update) a comment (requires authentication)
router.put('/:id', authenticate, (req, res) => {
const { content } = req.body;
const commentId = req.params.id;
// Find the comment in our data store
const allComments = comments.getAll();
const comment = allComments.find(c => c.id === commentId);
if (!comment) {
return res.status(404).json({ message: 'Comment not found' });
}
// Only allow users to update their own comments
if (comment.author !== req.user.name && req.user.role !== 'admin') {
return res.status(403).json({ message: 'You can only update your own comments' });
}
const updatedComment = comments.update(commentId, { content });
res.json(updatedComment);
});
// DELETE a comment (requires authentication)
router.delete('/:id', authenticate, (req, res) => {
const commentId = req.params.id;
// Find the comment in our data store
const allComments = comments.getAll();
const comment = allComments.find(c => c.id === commentId);
if (!comment) {
return res.status(404).json({ message: 'Comment not found' });
}
// Only allow users to delete their own comments or admins to delete any
if (comment.author !== req.user.name && req.user.role !== 'admin') {
return res.status(403).json({ message: 'You can only delete your own comments' });
}
const success = comments.delete(commentId);
if (!success) {
return res.status(404).json({ message: 'Comment not found' });
}
res.status(204).end();
});
module.exports = router;
Step 5: Create the Main Application File
Create the app.js file in the root directory:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Import middleware
const logger = require('./middleware/logger');
const { errorHandler, notFoundHandler } = require('./middleware/errorHandler');
// Import routes
const postsRouter = require('./routes/posts');
const commentsRouter = require('./routes/comments');
// Apply global middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(logger);
// Apply routes
app.use('/api/posts', postsRouter);
app.use('/api/comments', commentsRouter);
// Root route
app.get('/', (req, res) => {
res.json({
message: 'Blog API',
endpoints: {
posts: '/api/posts',
comments: '/api/comments'
},
timestamp: req.requestTime // Added by logger middleware
});
});
// Handle 404 errors
app.use(notFoundHandler);
// Handle all other errors
app.use(errorHandler);
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
Step 6: Test Your API
- Start your server:
node app.js - Use a tool like Postman, Insomnia, or curl to test your API endpoints:
- GET all posts:
GET http://localhost:3000/api/posts - GET post by ID:
GET http://localhost:3000/api/posts/1 - CREATE post:
POST http://localhost:3000/api/postswith JSON body:{ "title": "Testing Express Middleware", "content": "This is a test post created via the API." }Don't forget to include the
x-api-key: secret-api-keyheader! - GET comments for a post:
GET http://localhost:3000/api/comments/post/1
- GET all posts:
Bonus Challenges
- Add Validation Middleware: Create a middleware to validate post and comment data
- Add Rate Limiting: Implement rate limiting for API endpoints
- Add Pagination: Update the posts and comments routes to support pagination
- Add Search Functionality: Add the ability to search posts by title or content
- Add Swagger Documentation: Document your API using Swagger/OpenAPI
Summary
- Routing is how Express handles requests to different URL paths and HTTP methods
- Route Parameters let you capture dynamic values from the URL path (
:paramName) - Query Parameters provide optional data in the URL after the
?symbol - Route Handlers are functions that execute when a matching route is found
- Router Object allows modular route handling through the
express.Router()class - Middleware are functions that run during the request-response cycle and have access to req, res, and next
- Built-in Middleware includes
express.json(),express.urlencoded(), andexpress.static() - Custom Middleware can be created for specific application needs like logging, authentication, etc.
- Third-party Middleware provides additional functionality through npm packages
- Middleware Execution Order is important as middleware runs sequentially based on how it's defined
Further Reading
Next Lesson Preview
In our next session, we'll explore Request and Response objects in Express.js in greater detail. We'll learn about the various properties and methods available in these objects, how to handle different types of request data, and how to format and send different types of responses. We'll also cover more advanced techniques for handling HTTP headers, cookies, sessions, and file uploads.