Skip to main content

Next.js API Middleware

API middleware is a powerful concept that allows you to intercept and process HTTP requests before they reach your API route handlers in Next.js. Middleware functions enable you to extract common logic such as authentication, logging, error handling, and request validation into reusable components that can be applied across multiple API routes.

Introduction to API Middleware

In Next.js, API routes are defined in the pages/api directory (or app/api when using the App Router). These routes handle incoming HTTP requests and provide responses. However, as your application grows, you'll often find yourself repeating the same code across routes for common tasks like:

  • Authenticating users
  • Validating request data
  • Logging requests
  • Setting CORS headers
  • Error handling

Middleware provides a solution to this repetition by allowing you to process requests before they reach your route handlers. It creates a pipeline where requests flow through your middleware before reaching the final handler.

Basic Middleware Structure

In Next.js, you can create middleware for API routes using a higher-order function pattern. Let's look at a simple example:

javascript
// A simple middleware function
export function withLogging(handler) {
return async (req, res) => {
// Code that runs before handling the request
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);

// Call the handler
return await handler(req, res);

// Note: Code here would run after the response has been sent
// which typically isn't useful for API middleware
};
}

Then you can use this middleware in your API route:

javascript
// pages/api/hello.js
import { withLogging } from '../../middleware/withLogging';

function helloHandler(req, res) {
res.status(200).json({ message: 'Hello World!' });
}

// Apply middleware to the handler
export default withLogging(helloHandler);

When a request is made to /api/hello, it will first pass through the withLogging middleware, which logs the request details, and then proceeds to the helloHandler function.

Chaining Multiple Middleware

Often you'll need to apply multiple middleware functions to a route. We can create a helper function to make this cleaner:

javascript
// middleware/index.js
export const applyMiddleware = (handler, middlewares = []) => {
return middlewares.reduceRight((acc, middleware) => {
return middleware(acc);
}, handler);
};

Now you can chain multiple middleware functions:

javascript
// pages/api/protected-resource.js
import { applyMiddleware } from '../../middleware';
import { withAuth } from '../../middleware/withAuth';
import { withLogging } from '../../middleware/withLogging';
import { withCors } from '../../middleware/withCors';

function protectedResourceHandler(req, res) {
res.status(200).json({ data: 'This is protected data' });
}

export default applyMiddleware(protectedResourceHandler, [
withLogging,
withAuth,
withCors,
]);

In this example, requests flow through the middleware in the order: CORS → Authentication → Logging → Handler.

Common Middleware Patterns

Let's examine some common middleware patterns you'll likely implement in your Next.js API routes.

Authentication Middleware

Authentication middleware verifies if a user is authorized to access a resource:

javascript
// middleware/withAuth.js
export function withAuth(handler) {
return async (req, res) => {
// Get the authorization token from headers
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized: Missing or invalid token' });
}

const token = authHeader.split(' ')[1];

try {
// Verify the token (example using a hypothetical verifyToken function)
const user = await verifyToken(token);

// Attach the user to the request object for use in the handler
req.user = user;

// Continue to the handler
return handler(req, res);
} catch (error) {
return res.status(401).json({ error: 'Unauthorized: Invalid token' });
}
};
}

Error Handling Middleware

Error handling middleware catches errors that occur during request processing:

javascript
// middleware/withErrorHandler.js
export function withErrorHandler(handler) {
return async (req, res) => {
try {
return await handler(req, res);
} catch (error) {
console.error('API Error:', error);

// Send an appropriate response based on the error
if (error.name === 'ValidationError') {
return res.status(400).json({ error: 'Validation failed', details: error.message });
}

if (error.code === 'UNAUTHORIZED') {
return res.status(401).json({ error: 'Unauthorized' });
}

// Default error response
return res.status(500).json({ error: 'Internal server error' });
}
};
}

Rate Limiting Middleware

To prevent abuse of your API, you can implement rate limiting:

javascript
// middleware/withRateLimit.js
const rateLimit = new Map(); // In production, use Redis or another persistent store

export function withRateLimit(limit = 10, windowMs = 60000) {
return (handler) => {
return (req, res) => {
// Get client identifier (IP address or API key)
const clientId = req.headers['x-api-key'] || req.socket.remoteAddress;

// Get current timestamp
const now = Date.now();

// Initialize or get client's request history
if (!rateLimit.has(clientId)) {
rateLimit.set(clientId, []);
}

// Get requests within the time window
const clientRequests = rateLimit.get(clientId);
const requestsWithinWindow = clientRequests
.filter(timestamp => now - timestamp < windowMs);

// Update the client's request history
rateLimit.set(clientId, [...requestsWithinWindow, now]);

// Check if limit exceeded
if (requestsWithinWindow.length >= limit) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil((windowMs - (now - requestsWithinWindow[0])) / 1000)
});
}

// Continue to the handler
return handler(req, res);
};
};
}

Request Validation Middleware

Validate incoming request data before processing:

javascript
// middleware/withValidation.js
export function withValidation(validationSchema) {
return (handler) => {
return async (req, res) => {
try {
// Validate request body against schema
if (req.body) {
await validationSchema.validate(req.body, { abortEarly: false });
}

// Continue to the handler
return handler(req, res);
} catch (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
};
};
}

Real-world Application: Building a Complete API Endpoint

Let's combine these middleware functions to create a complete API endpoint for creating a new user:

javascript
// pages/api/users/create.js
import { applyMiddleware } from '../../../middleware';
import { withLogging } from '../../../middleware/withLogging';
import { withErrorHandler } from '../../../middleware/withErrorHandler';
import { withRateLimit } from '../../../middleware/withRateLimit';
import { withValidation } from '../../../middleware/withValidation';
import { withCors } from '../../../middleware/withCors';
import * as yup from 'yup';

// Define validation schema
const userSchema = yup.object().shape({
username: yup.string().required().min(3).max(20),
email: yup.string().email().required(),
password: yup.string().required().min(8),
});

async function createUserHandler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method Not Allowed' });
}

// Process the request
const { username, email, password } = req.body;

// Here we would typically hash the password and save to database
// const hashedPassword = await bcrypt.hash(password, 10);
// const user = await db.users.create({ username, email, password: hashedPassword });

// For demonstration, we'll just respond with success
return res.status(201).json({
message: 'User created successfully',
user: { username, email, id: 'new-user-123' }
});
}

export default applyMiddleware(createUserHandler, [
withCors,
withLogging,
withErrorHandler,
withRateLimit(5, 60000), // 5 requests per minute
withValidation(userSchema)
]);

Global API Middleware with Next.js Middleware

Starting with Next.js 12, you can also create a global middleware that runs for all routes, including API routes. This is defined in a middleware.ts (or .js) file in your project root:

javascript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Example: Add a custom header to all API responses
if (request.nextUrl.pathname.startsWith('/api/')) {
const response = NextResponse.next();
response.headers.set('x-api-version', '1.0');
return response;
}

return NextResponse.next();
}

// Configure which paths this middleware is applied to
export const config = {
matcher: '/api/:path*',
};

This global middleware approach is particularly useful for concerns that apply to all or most of your API routes, such as setting common headers, logging, or basic security checks.

Best Practices for API Middleware

  1. Keep middleware focused: Each middleware should have a single responsibility.

  2. Order matters: Consider the sequence of middleware execution. For example, authentication should often come before validation.

  3. Error handling: Always implement proper error handling in middleware to prevent request processing from halting unexpectedly.

  4. Avoid side effects: Be careful with middleware that modifies the global state, as it may impact other requests.

  5. Type safety: If using TypeScript, define proper types for your middleware functions to improve code quality.

  6. Testing: Write unit tests for your middleware functions to ensure they behave as expected.

Summary

API middleware in Next.js provides a powerful way to structure your API routes by separating common concerns into reusable pieces. This approach:

  • Reduces code duplication
  • Improves maintainability
  • Enforces consistent behavior across API routes
  • Makes it easier to reason about request processing flow

By composing middleware functions, you can create a robust API foundation that handles authentication, validation, error handling, and other cross-cutting concerns in a modular way.

Additional Resources

Exercises

  1. Create a middleware function that logs the request duration by capturing timestamps before and after handler execution.

  2. Implement a caching middleware that stores API responses in memory for a configurable duration.

  3. Build a content-type validation middleware that ensures incoming requests have the correct Content-Type header.

  4. Create a middleware that implements conditional rate limiting based on user roles (e.g., higher limits for premium users).

  5. Combine multiple middleware functions to create a secure API endpoint that requires authentication, validates input, and handles errors gracefully.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)