Building robust APIs with TypeScript and Express requires a solid understanding of middleware. In this comprehensive guide, we’ll explore how to create, implement, and optimize middleware in your TypeScript Express applications.
Middleware functions are the backbone of Express applications, acting as intermediary processing layers that handle requests before they reach their final route handlers. With TypeScript, we can add type safety to our middleware, making our applications more reliable and maintainable.
Table of Contents
- Understanding Express Middleware
- Creating Type-Safe Middleware
- Error Handling Middleware
- Request Validation Middleware
- Chaining Multiple Middleware
- Performance Monitoring Middleware
- Best Practices
- Common Use Cases
- Conclusion
Understanding Express Middleware
Middleware functions in Express have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. These functions can execute any code, modify request and response objects, end the request-response cycle, or call the next middleware function.
interface RequestHandler {
(req: Request, res: Response, next: NextFunction): void;
}
Code language: PHP (php)
Creating Type-Safe Middleware
Let’s start with a basic authentication middleware example:
import { Request, Response, NextFunction } from 'express';
interface AuthenticatedRequest extends Request {
user?: {
id: string;
role: string;
};
}
const authMiddleware = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
try {
// Verify token and attach user to request
req.user = {
id: '123',
role: 'admin'
};
next();
} catch (error) {
res.status(401).json({ message: 'Invalid token' });
}
};
Code language: PHP (php)
Error Handling Middleware
Error handling middleware is special in Express as it takes an additional error parameter:
import { ErrorRequestHandler } from 'express';
interface CustomError extends Error {
statusCode?: number;
}
const errorHandler: ErrorRequestHandler = (
err: CustomError,
req: Request,
res: Response,
next: NextFunction
) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
success: false,
error: message
});
};
Code language: PHP (php)
Request Validation Middleware
Implementing request validation using a middleware pattern:
import { z } from 'zod';
const userSchema = z.object({
username: z.string().min(3),
email: z.string().email(),
age: z.number().min(18)
});
const validateUserInput = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
userSchema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({
error: 'Validation failed',
details: error.errors
});
} else {
next(error);
}
}
};
Code language: JavaScript (javascript)
Chaining Multiple Middleware
You can chain multiple middleware functions for complex request processing:
import express from 'express';
const app = express();
app.post(
'/api/users',
authMiddleware,
validateUserInput,
async (req: Request, res: Response) => {
// Route handler implementation
res.json({ message: 'User created successfully' });
}
);
Code language: JavaScript (javascript)
Performance Monitoring Middleware
Create middleware to monitor API performance:
const performanceMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
const start = process.hrtime();
res.on('finish', () => {
const [seconds, nanoseconds] = process.hrtime(start);
const duration = seconds * 1000 + nanoseconds / 1000000;
console.log(`${req.method} ${req.url} - ${duration}ms`);
});
next();
};
Code language: JavaScript (javascript)
Best Practices
1. Keep Middleware Focused
Each middleware function should have a single responsibility. This makes your code easier to test and maintain:
// Good: Focused middleware
const corsMiddleware = (req: Request, res: Response, next: NextFunction) => {
res.header('Access-Control-Allow-Origin', '*');
next();
};
Code language: JavaScript (javascript)
2. Use Async/Await
When dealing with asynchronous operations, use async/await for better error handling:
const asyncMiddleware = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
await someAsyncOperation();
next();
} catch (error) {
next(error);
}
};
Code language: JavaScript (javascript)
3. Proper Error Propagation
Always use the next function to propagate errors:
const errorPropagationMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
// Some operation that might fail
next();
} catch (error) {
next(error); // Properly propagate the error
}
};
Code language: PHP (php)
Common Use Cases
Rate Limiting
Implement rate limiting to protect your API:
import rateLimit from 'express-rate-limit';
const rateLimitMiddleware = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later'
});
app.use('/api/', rateLimitMiddleware);
Code language: JavaScript (javascript)
Request Logging
Implement detailed request logging:
const requestLogger = (
req: Request,
res: Response,
next: NextFunction
) => {
const timestamp = new Date().toISOString();
console.log(`${timestamp} - ${req.method} ${req.url}`);
console.log('Headers:', req.headers);
console.log('Body:', req.body);
next();
};
Code language: JavaScript (javascript)
Conclusion
TypeScript Express middleware provides a powerful way to handle common functionality in your API applications. By following these patterns and best practices, you can create maintainable, type-safe middleware that enhances your application’s functionality.
Remember to:
- Keep middleware functions focused and single-purpose
- Properly type your request and response objects
- Handle errors appropriately
- Use async/await for asynchronous operations
- Chain middleware effectively for complex operations
For more advanced TypeScript patterns, check out our guide on TypeScript Generic Functions which can help you create even more flexible and reusable middleware components.