How to Chain Multiple Next Middleware Functions in a Single `middleware.ts` File
You can chain multiple Next middleware functions by composing small, single-purpose helper functions inside the exported middleware function and short-circuiting the execution chain whenever a helper returns anything other than NextResponse.next().
When building route protections in a Next.js application, you often need to enforce authentication, rate limiting, and feature flags simultaneously. Since the framework only allows one exported middleware function per middleware.ts file, understanding how to effectively chain multiple next middleware functions becomes essential for maintainable security layers. According to the vercel/next.js source code, the runtime treats NextResponse.next() as a "continue" signal while any other response terminates the chain.
Understanding the Single-File Constraint
Next.js loads middleware.ts (or .js) as the sole entry point for edge middleware. The framework expects exactly one exported async function named middleware that receives a NextRequest and returns a Response or NextResponse.
In packages/next/src/server/web/adapter.ts, the adapter invokes your middleware and inspects the returned value. Lines 484-487 demonstrate that when the returned response is not a "continue" signal, the adapter immediately finalizes the result, preventing further routing phases. This architectural decision enforces the single-handler constraint while allowing internal composition.
The Chaining Pattern: Helper Functions and Short-Circuit Logic
To chain multiple next middleware functions effectively, implement the short-circuit pattern:
- Create focused helper functions for each protection layer (auth, rate limiting, IP blocking).
- Execute them sequentially inside the main
middlewarefunction. - Check each return value: if it is not
NextResponse.next(), return it immediately to stop the chain. - Proceed to the next helper only if the current one returns
NextResponse.next().
The internal mechanism that enables this behavior resides in packages/next-routing/src/middleware.ts, specifically the responseToMiddlewareResult function (lines 13-98). This utility converts your Response into a MiddlewareResult. When the result type is "continue"—which occurs only when you return NextResponse.next()—the router proceeds to the next phase. Any other result type (redirect, rewrite, or custom response) terminates processing.
Complete Implementation Example
Below is a production-ready middleware.ts file demonstrating how to chain authentication, rate limiting, and feature-flag guards.
Authentication Guard
// Helper to enforce session validation
async function requireAuth(req: NextRequest): Promise<NextResponse> {
const token = req.cookies.get('auth-token')?.value;
if (!token) {
const url = req.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('callbackUrl', req.nextUrl.pathname);
return NextResponse.redirect(url);
}
return NextResponse.next();
}
Rate Limiting
// Simple in-memory rate limiter (use Redis in production)
const ipCounters = new Map<string, { count: number; reset: number }>();
const MAX_REQUESTS = 100;
const WINDOW_MS = 60_000;
async function rateLimit(req: NextRequest): Promise<NextResponse> {
const ip = req.ip ?? 'unknown';
const now = Date.now();
const entry = ipCounters.get(ip) ?? { count: 0, reset: now + WINDOW_MS };
if (now > entry.reset) {
entry.count = 0;
entry.reset = now + WINDOW_MS;
}
entry.count++;
ipCounters.set(ip, entry);
if (entry.count > MAX_REQUESTS) {
return new NextResponse('Too many requests', { status: 429 });
}
return NextResponse.next();
}
Feature Flag Protection
// Guard for beta routes based on environment variables
function betaGuard(req: NextRequest): Promise<NextResponse> {
const isBetaEnabled = process.env.NEXT_PUBLIC_ENABLE_BETA === 'true';
if (req.nextUrl.pathname.startsWith('/beta') && !isBetaEnabled) {
return Promise.resolve(
new NextResponse('Beta feature disabled', { status: 403 })
);
}
return Promise.resolve(NextResponse.next());
}
Composing the Chain
// /middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest): Promise<NextResponse> {
// Execute helpers sequentially, short-circuiting on first non-next response
const authResult = await requireAuth(req);
if (authResult !== NextResponse.next()) return authResult;
const rateResult = await rateLimit(req);
if (rateResult !== NextResponse.next()) return rateResult;
const betaResult = await betaGuard(req);
if (betaResult !== NextResponse.next()) return betaResult;
// All protections passed; proceed to route handler
return NextResponse.next();
}
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/'],
};
How the Short-Circuit Mechanism Works
The Next.js runtime determines whether to continue processing based on the return value of your middleware function. As implemented in packages/next/src/server/web/adapter.ts, the adapter converts your Response into an internal result object.
| Return Value | Internal Result | Behavior |
|---|---|---|
NextResponse.next() |
"continue" |
Proceed to next middleware phase or route handler |
NextResponse.redirect() |
"redirect" |
Terminate chain, apply redirect |
NextResponse.rewrite() |
"rewrite" |
Terminate chain, apply rewrite |
Custom Response |
"response" |
Terminate chain, return custom response |
When you chain multiple next middleware functions using the helper pattern, you manually replicate this logic by checking if a helper returns NextResponse.next(). If it returns anything else, you immediately return that value, effectively terminating the chain and preventing subsequent checks from executing.
Summary
- Single export constraint: Next.js only allows one
middlewareexport permiddleware.tsfile, enforced by the runtime adapter inpackages/next/src/server/web/adapter.ts. - Composition pattern: Chain multiple next middleware functions by creating single-purpose helpers and executing them sequentially inside the main export.
- Short-circuit logic: Return immediately when any helper returns a response other than
NextResponse.next(), mirroring the internalresponseToMiddlewareResultbehavior inpackages/next-routing/src/middleware.ts. - Type safety: Use
NextRequestandNextResponsetypes fromnext/serverto ensure full compatibility with the edge runtime.
Frequently Asked Questions
Can I export multiple middleware functions from separate files and import them into middleware.ts?
Yes, you can define helper functions in separate files (e.g., lib/middleware/auth.ts, lib/middleware/rateLimit.ts) and import them into your root middleware.ts file. However, you must still export only one middleware function that orchestrates these helpers. The framework only recognizes the default export from middleware.ts as the entry point.
How do I conditionally apply specific middleware to certain routes only?
Use the matcher configuration in your middleware.ts export to define which paths trigger the middleware. For route-specific logic inside the middleware function, inspect req.nextUrl.pathname and return NextResponse.next() early for paths that should skip certain checks. This allows you to maintain one middleware file while applying different protection layers to different route patterns.
What happens if I forget to return NextResponse.next() from a helper?
If a helper function returns undefined or null instead of a NextResponse, the main middleware function will receive an invalid value. When you attempt to compare this with NextResponse.next() or return it to the Next.js runtime, you may encounter TypeScript errors or runtime failures. Always ensure helpers return either NextResponse.next() to continue or a specific response (redirect, rewrite, error) to terminate the chain.
Is there a performance limit to how many middleware functions I can chain?
There is no explicit limit on the number of helper functions you can chain, but each function adds latency to the request lifecycle. Since middleware runs on the edge runtime, you should prioritize async operations and avoid heavy computation. If you find yourself chaining more than five or six complex checks, consider consolidating related logic or using a dedicated authorization service to keep the middleware lightweight and maintainable.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →