Express.js Framework and Middleware
60 minExpress.js is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
Middleware functions have access to the request object (req), response object (res), and the next middleware function in the application's request-response cycle.
Express.js provides built-in middleware for common tasks like parsing request bodies, serving static files, and handling CORS.
Understanding middleware order and custom middleware creation is crucial for building maintainable Express applications.
Middleware executes in the order it's defined, making order critical. Middleware that should run for all routes should be defined before route-specific middleware.
Error-handling middleware has four parameters (err, req, res, next) and should be defined after all other middleware and routes to catch errors from anywhere in the application.
Key Concepts
- Express.js is a minimal web framework for Node.js.
- Middleware functions process requests before they reach route handlers.
- Middleware order matters - they execute sequentially.
- Error-handling middleware catches errors from the application.
- Express provides built-in middleware for common tasks.
Learning Objectives
Master
- Setting up Express.js applications
- Creating and using custom middleware
- Understanding middleware execution order
- Implementing error-handling middleware
Develop
- Middleware architecture thinking
- Request/response cycle understanding
- Error handling strategies
Tips
- Define middleware in the correct order - global middleware before routes.
- Always call next() in middleware unless you're ending the request.
- Use express.json() and express.urlencoded() for parsing request bodies.
- Implement error-handling middleware as the last middleware.
Common Pitfalls
- Forgetting to call next() in middleware, causing requests to hang.
- Placing middleware in the wrong order, breaking functionality.
- Not handling errors properly, causing unhandled promise rejections.
- Using '*' for CORS origins in production (security risk).
Summary
- Express.js provides a minimal framework for Node.js web applications.
- Middleware processes requests before route handlers.
- Middleware order is critical for correct behavior.
- Error-handling middleware should be defined last.
Exercise
Create a comprehensive Express.js application with custom middleware, route organization, and error handling.
const express = require('express');
const path = require('path');
const fs = require('fs').promises;
class ExpressApp {
constructor() {
this.app = express();
this.port = process.env.PORT || 3000;
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
}
// Setup middleware
setupMiddleware() {
// Custom logging middleware
this.app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`);
});
next();
});
// Request parsing middleware
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// CORS middleware
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// Static file serving
this.app.use('/static', express.static(path.join(__dirname, 'public')));
// Security middleware
this.app.use((req, res, next) => {
// Remove X-Powered-By header
res.removeHeader('X-Powered-By');
// Add security headers
res.header('X-Content-Type-Options', 'nosniff');
res.header('X-Frame-Options', 'DENY');
res.header('X-XSS-Protection', '1; mode=block');
next();
});
// Rate limiting middleware
this.setupRateLimiting();
}
// Rate limiting implementation
setupRateLimiting() {
const requestCounts = new Map();
const WINDOW_MS = 15 * 60 * 1000; // 15 minutes
const MAX_REQUESTS = 100; // Max requests per window
this.app.use((req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
const now = Date.now();
if (!requestCounts.has(clientIP)) {
requestCounts.set(clientIP, { count: 1, resetTime: now + WINDOW_MS });
} else {
const clientData = requestCounts.get(clientIP);
if (now > clientData.resetTime) {
// Reset window
clientData.count = 1;
clientData.resetTime = now + WINDOW_MS;
} else {
clientData.count++;
if (clientData.count > MAX_REQUESTS) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil((clientData.resetTime - now) / 1000)
});
}
}
}
next();
});
}
// Setup routes
setupRoutes() {
// API routes
this.app.use('/api/users', this.createUserRoutes());
this.app.use('/api/products', this.createProductRoutes());
// Health check
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
});
// Root route
this.app.get('/', (req, res) => {
res.json({
message: 'Welcome to Express.js Application',
version: '1.0.0',
endpoints: {
health: '/health',
users: '/api/users',
products: '/api/products'
}
});
});
// 404 handler
this.app.use('*', (req, res) => {
res.status(404).json({
error: 'Route not found',
path: req.originalUrl,
method: req.method
});
});
}
// User routes
createUserRoutes() {
const router = express.Router();
const users = new Map();
let nextId = 1;
// Validation middleware
const validateUser = (req, res, next) => {
const { name, email, age } = req.body;
const errors = [];
if (!name || name.trim().length < 2) {
errors.push('Name must be at least 2 characters long');
}
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push('Valid email is required');
}
if (!age || age < 13 || age > 120) {
errors.push('Age must be between 13 and 120');
}
if (errors.length > 0) {
return res.status(400).json({ errors });
}
next();
};
// Authentication middleware (simplified)
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const token = authHeader.substring(7);
// Simple token validation (in real apps, use JWT)
if (token !== 'valid-token') {
return res.status(401).json({ error: 'Invalid token' });
}
next();
};
// GET /api/users
router.get('/', (req, res) => {
const { page = 1, limit = 10, search } = req.query;
let filteredUsers = Array.from(users.values());
// Search functionality
if (search) {
filteredUsers = filteredUsers.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase()) ||
user.email.toLowerCase().includes(search.toLowerCase())
);
}
// Pagination
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
res.json({
users: paginatedUsers,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredUsers.length,
pages: Math.ceil(filteredUsers.length / limit)
}
});
});
// GET /api/users/:id
router.get('/:id', (req, res) => {
const user = users.get(parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// POST /api/users
router.post('/', validateUser, (req, res) => {
const user = {
id: nextId++,
...req.body,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
users.set(user.id, user);
res.status(201).json(user);
});
// PUT /api/users/:id
router.put('/:id', authenticate, validateUser, (req, res) => {
const userId = parseInt(req.params.id);
const user = users.get(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const updatedUser = {
...user,
...req.body,
updatedAt: new Date().toISOString()
};
users.set(userId, updatedUser);
res.json(updatedUser);
});
// DELETE /api/users/:id
router.delete('/:id', authenticate, (req, res) => {
const userId = parseInt(req.params.id);
if (!users.has(userId)) {
return res.status(404).json({ error: 'User not found' });
}
users.delete(userId);
res.status(204).send();
});
return router;
}
// Product routes
createProductRoutes() {
const router = express.Router();
const products = new Map();
let nextId = 1;
// GET /api/products
router.get('/', (req, res) => {
const { category, minPrice, maxPrice, sort = 'name' } = req.query;
let filteredProducts = Array.from(products.values());
// Filter by category
if (category) {
filteredProducts = filteredProducts.filter(p => p.category === category);
}
// Filter by price range
if (minPrice) {
filteredProducts = filteredProducts.filter(p => p.price >= parseFloat(minPrice));
}
if (maxPrice) {
filteredProducts = filteredProducts.filter(p => p.price <= parseFloat(maxPrice));
}
// Sorting
filteredProducts.sort((a, b) => {
if (sort === 'price') {
return a.price - b.price;
} else if (sort === 'price-desc') {
return b.price - a.price;
} else {
return a.name.localeCompare(b.name);
}
});
res.json(filteredProducts);
});
// POST /api/products
router.post('/', (req, res) => {
const { name, price, category, description } = req.body;
if (!name || !price || !category) {
return res.status(400).json({ error: 'Name, price, and category are required' });
}
const product = {
id: nextId++,
name,
price: parseFloat(price),
category,
description: description || '',
createdAt: new Date().toISOString()
};
products.set(product.id, product);
res.status(201).json(product);
});
return router;
}
// Error handling middleware
setupErrorHandling() {
// Global error handler
this.app.use((error, req, res, next) => {
console.error('Error:', error);
if (error.type === 'entity.parse.failed') {
return res.status(400).json({ error: 'Invalid JSON' });
}
if (error.type === 'entity.too.large') {
return res.status(413).json({ error: 'Request entity too large' });
}
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong'
});
});
}
// Start the server
start() {
this.app.listen(this.port, () => {
console.log(`🚀 Express server running on port ${this.port}`);
console.log(`📱 Health check: http://localhost:${this.port}/health`);
console.log(`👥 Users API: http://localhost:${this.port}/api/users`);
console.log(`🛍️ Products API: http://localhost:${this.port}/api/products`);
});
}
}
// Run the application
if (require.main === module) {
const app = new ExpressApp();
app.start();
}
module.exports = ExpressApp;
Exercise Tips
- Organize routes in separate files using express.Router().
- Use middleware for authentication, logging, and validation.
- Implement proper error handling with try-catch and error middleware.
- Test your API endpoints with tools like Postman or curl.