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:

  1. Create focused helper functions for each protection layer (auth, rate limiting, IP blocking).
  2. Execute them sequentially inside the main middleware function.
  3. Check each return value: if it is not NextResponse.next(), return it immediately to stop the chain.
  4. 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 middleware export per middleware.ts file, enforced by the runtime adapter in packages/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 internal responseToMiddlewareResult behavior in packages/next-routing/src/middleware.ts.
  • Type safety: Use NextRequest and NextResponse types from next/server to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →