TypeScript Express Middleware: Complete Guide for API Development

typescript-express-middleware

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.

Snippettext
interface RequestHandler {
  (req: Request, res: Response, next: NextFunction): void;
}

Creating Type-Safe Middleware

Let’s start with a basic authentication middleware example:

Snippettext
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' });
  }
};

Error Handling Middleware

Error handling middleware is special in Express as it takes an additional error parameter:

Snippettext
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
  });
};

Request Validation Middleware

Implementing request validation using a middleware pattern:

Snippettext
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);
    }
  }
};

Chaining Multiple Middleware

You can chain multiple middleware functions for complex request processing:

Snippettext
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' });
  }
);

Performance Monitoring Middleware

Create middleware to monitor API performance:

Snippettext
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();
};

Best Practices

1. Keep Middleware Focused

Each middleware function should have a single responsibility. This makes your code easier to test and maintain:

Snippettext
// Good: Focused middleware
const corsMiddleware = (req: Request, res: Response, next: NextFunction) => {
  res.header('Access-Control-Allow-Origin', '*');
  next();
};

2. Use Async/Await

When dealing with asynchronous operations, use async/await for better error handling:

Snippettext
const asyncMiddleware = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    await someAsyncOperation();
    next();
  } catch (error) {
    next(error);
  }
};

3. Proper Error Propagation

Always use the next function to propagate errors:

Snippettext
const errorPropagationMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    // Some operation that might fail
    next();
  } catch (error) {
    next(error); // Properly propagate the error
  }
};

Common Use Cases

Rate Limiting

Implement rate limiting to protect your API:

Snippettext
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);

Request Logging

Implement detailed request logging:

Snippettext
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();
};

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.

whoami

Stefan

Stefan founded Ceos3c in 2016 and has spent a decade teaching cybersecurity, Linux, and web development through hands-on tutorials. He builds and ships real products while sharing his journey: ProofSwap.me, XPMetric.com, and EasyEarningsTracker.com. Find him on YouTube (youtube.com/stefanrows) and X (@StefanRows).

whoami

Stefan

Stefan founded Ceos3c in 2016 and has spent a decade teaching cybersecurity, Linux, and web development through hands-on tutorials. He builds and ships real products while sharing his journey: ProofSwap.me, XPMetric.com, and EasyEarningsTracker.com. Find him on YouTube (youtube.com/stefanrows) and X (@StefanRows).