# How to Chain Multiple Next Middleware Functions in a Single `middleware.ts` File

> Learn to effectively chain multiple Next middleware functions in a single middleware.ts file. Master route protections by composing helper functions and short-circuiting execution for streamlined security.

- Repository: [Vercel/next.js](https://github.com/vercel/next.js)
- Tags: how-to-guide
- Published: 2026-02-21

---

**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`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/middleware.ts) file demonstrating how to chain authentication, rate limiting, and feature-flag guards.

### Authentication Guard

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// /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`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/middleware.ts) file, enforced by the runtime adapter in [`packages/next/src/server/web/adapter.ts`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/middleware.ts)?

Yes, you can define helper functions in separate files (e.g., [`lib/middleware/auth.ts`](https://github.com/vercel/next.js/blob/main/lib/middleware/auth.ts), [`lib/middleware/rateLimit.ts`](https://github.com/vercel/next.js/blob/main/lib/middleware/rateLimit.ts)) and import them into your root [`middleware.ts`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/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`](https://github.com/vercel/next.js/blob/main/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.