TypeScript Express Middleware: Complete Guide for API Development

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

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.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap