---
name: routing-middleware
description: Vercel Routing Middleware guidance — request interception before cache, rewrites, redirects, personalization. Works with any framework. Supports Edge, Node.js, and Bun runtimes. Use when intercepting requests at the platform level.
metadata:
  priority: 6
  docs:
    - "https://nextjs.org/docs/app/api-reference/file-conventions/proxy"
    - "https://vercel.com/docs/routing-middleware"
  sitemap: "https://nextjs.org/sitemap.xml"
  pathPatterns:
    - 'middleware.ts'
    - 'middleware.js'
    - 'middleware.mts'
    - 'middleware.mjs'
    - 'proxy.ts'
    - 'proxy.js'
    - 'proxy.mts'
    - 'proxy.mjs'
    - 'src/middleware.ts'
    - 'src/middleware.js'
    - 'src/middleware.mts'
    - 'src/middleware.mjs'
    - 'src/proxy.ts'
    - 'src/proxy.js'
    - 'src/proxy.mts'
    - 'src/proxy.mjs'
    - 'vercel.json'
    - 'apps/*/vercel.json'
    - 'vercel.ts'
    - 'vercel.mts'
  bashPatterns:
    - '\bnpx\s+@vercel/config\b'
validate:
  - pattern: 'NextResponse.*from\s+[''"]next/server[''"]|from\s+[''"]next/server[''"].*NextResponse'
    message: 'Next.js middleware.ts is renamed to proxy.ts in Next.js 16 — rename the file and use the Node.js runtime. See the proxy file convention in the bundled docs at node_modules/next/dist/docs/.'
    severity: recommended
    skipIfFileContains: 'proxy\.ts|runtime.*nodejs'
retrieval:
  aliases:
    - request interceptor
    - middleware
    - rewrite rules
    - redirect rules
  intents:
    - intercept requests
    - add middleware
    - configure rewrites
    - set up redirects
  entities:
    - middleware
    - rewrite
    - redirect
    - personalization
    - Edge
chainTo:
  - pattern: 'from\s+[''""]next-auth[''""]'
    targetSkill: auth
    message: 'Auth logic in middleware — loading Auth guidance for Clerk/Auth0 integration patterns.'
  - pattern: 'from\s+[''""](jsonwebtoken)[''""]|jwt\.(verify|decode)\('
    targetSkill: auth
    message: 'Manual JWT verification in middleware — loading Auth guidance for managed auth middleware patterns (Clerk, Descope).'
    skipIfFileContains: 'clerkMiddleware|@clerk/|@auth0/'
title: routing-middleware
canonical_url: https://skilld.dev/gh/vercel-labs/vercel-plugin/routing-middleware
last_updated: 2026-10-01T12:02:54.000Z
---

> **Skill from skilld.dev.** Follow the instructions below for this session. You do not need to install anything.
>
> If the user asked to install this Skill, run `npx skilld install vercel-labs/vercel-plugin/routing-middleware`. Install writes the Skill files into the project, so every session loads them.

# Vercel Routing Middleware

You are an expert in Vercel Routing Middleware — the platform-level request interception layer.

## What It Is

Routing Middleware runs **before the cache** on every request matching its config. It is a **Vercel platform** feature (not framework-specific) that works with Next.js, SvelteKit, Astro, Nuxt, or any deployed framework. Built on Fluid Compute.

- **Preferred platform configuration**: Set `proxy.entrypoint` in `vercel.json`. The entrypoint can use any supported filename or directory and runs on Node.js. Frameworks that build their own routing middleware (Next.js, Astro) do not use the `proxy` property; use the framework's file convention instead.
- **File convention**: `middleware.ts` or `middleware.js` at the project root. This convention defaults to Edge; set `runtime: 'nodejs'` to use Node.js.
- **Next.js 16**: Use `proxy.ts` and export `proxy`. Next.js Proxy runs on Node.js only.

## CRITICAL: Middleware Disambiguation

There are THREE "middleware" concepts in the Vercel ecosystem:

| Concept | File | Runtime | Scope | When to Use |
|---------|------|---------|-------|-------------|
| **Vercel Routing Middleware** | `proxy.entrypoint` or `middleware.ts` | Node/Edge/Bun | Any framework, platform-level | Request interception before cache: rewrites, redirects, geo, A/B |
| **Next.js 16 Proxy** | `proxy.ts` (root, or `src/proxy.ts` if using `--src-dir`) | Node.js only | Next.js 16+ only | Network-boundary proxy needing full Node APIs. NOT for auth. |
| **Vercel Functions** | Route or function file | Node/Bun/Python/Rust | General-purpose | Request handlers and backend compute, not an interception layer |

**Why the rename in Next.js 16** (`middleware.ts` → `proxy.ts`): "middleware" was often confused with Express.js middleware, and Next.js recommends the feature only as a last resort while it builds better APIs; "proxy" says what it is, a network boundary in front of the app. The exported function must also be renamed from `middleware` to `proxy`. Migration codemod: `npx @next/codemod@latest middleware-to-proxy .`

**Deprecation**: Next.js 16 still accepts `middleware.ts` but treats it as deprecated and logs a warning. It will be removed in a future version.

## Bun Runtime

To run Routing Middleware (and all Vercel Functions) on Bun, add `bunVersion` to `vercel.json`:

```json filename="vercel.json"
{
  "bunVersion": "1.x"
}
```

Set the middleware runtime to `nodejs` — Bun replaces the Node.js runtime transparently:

```ts
export const config = {
  runtime: 'nodejs', // Bun swaps in when bunVersion is set
};
```

Bun reduces average latency by ~28% in CPU-bound workloads. Currently in Public Beta — supports Next.js, Express, Hono, and Nitro.

## Basic Example

Configure an explicit entrypoint for framework-agnostic Routing Middleware:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "proxy": {
    "entrypoint": "proxy.ts",
    "matcher": ["/((?!_next/static|favicon.ico).*)"]
  }
}
```

```ts
// proxy.ts
import { geolocation, rewrite } from '@vercel/functions';

export default function proxy(request: Request) {
  const { country } = geolocation(request);
  const url = new URL(request.url);
  url.pathname = country === 'US' ? '/us' + url.pathname : '/intl' + url.pathname;
  return rewrite(url);
}
```

## Helper Methods (`@vercel/functions`)

For non-Next.js frameworks, import from `@vercel/functions`:

| Helper | Purpose |
|--------|---------|
| `next()` | Continue middleware chain (optionally modify headers) |
| `rewrite(url)` | Transparently serve content from a different URL |
| `geolocation(request)` | Get `city`, `country`, `latitude`, `longitude`, `region` |
| `ipAddress(request)` | Get client IP address |
| `waitUntil(promise)` | Keep function running after response is sent |

For Next.js, `NextResponse` provides `next()`, `rewrite()`, and `redirect()`. Use `geolocation(request)` and `ipAddress(request)` from `@vercel/functions`; `NextRequest.geo` and `NextRequest.ip` were removed in Next.js 15.

## Matcher Configuration

Middleware runs on **every route** by default. Use `config.matcher` to scope it:

```ts
// Single path
export const config = { matcher: '/dashboard/:path*' };

// Multiple paths
export const config = { matcher: ['/dashboard/:path*', '/api/:path*'] };

// Regex: exclude static files
export const config = {
  matcher: ['/((?!_next/static|favicon.ico).*)'],
};
```

**Tip**: Using `matcher` is preferred — unmatched paths skip middleware invocation entirely (saves compute).

## Common Patterns

### IP-Based Header Injection

```ts
import { ipAddress, next } from '@vercel/functions';

export default function middleware(request: Request) {
  return next({ headers: { 'x-real-ip': ipAddress(request) || 'unknown' } });
}
```

### A/B Testing via Global Config

```ts
import { get } from '@vercel/global-config';
import { rewrite } from '@vercel/functions';

export default async function middleware(request: Request) {
  const variant = await get('experiment-homepage'); // <1ms read
  const url = new URL(request.url);
  url.pathname = variant === 'B' ? '/home-b' : '/home-a';
  return rewrite(url);
}
```

### Background Processing

```ts
import { waitUntil } from '@vercel/functions';

export default function middleware(request: Request) {
  waitUntil(
    fetch('https://analytics.example.com/log', { method: 'POST', body: request.url })
  );
  return new Response('OK');
}
```

## Request Limits

| Limit | Value |
|-------|-------|
| Max URL length | 14 KB |
| Max request body | 4 MB |
| Max request headers | 64 headers / 16 KB total |

## Three CDN Routing Mechanisms

Vercel's CDN supports three routing mechanisms, evaluated in this order:

| Order | Mechanism | Scope | Deploy Required | How to Configure |
|-------|-----------|-------|-----------------|------------------|
| 1 | **Bulk Redirects** | Up to 1M static path→path redirects | No (runtime via Dashboard/API/CLI) | Dashboard, CSV upload, REST API |
| 2 | **Project-Level Routes** | Headers, rewrites, redirects | No (instant publish) | Dashboard, REST API, `vercel routes` CLI |
| 3 | **Deployment Config Routes** | Full routing rules | Yes (deploy) | `vercel.json`, `vercel.ts`, `next.config.ts` |

**Project-level routes** (added March 2026) let you update routing rules — response headers, rewrites to external APIs — without triggering a new deployment. They run after bulk redirects and before deployment config routes. Available on all plans.

### Project-Level Routes — Configuration Methods

Project-level routes take effect instantly (no deploy required). Three ways to manage them:

| Method | How |
|--------|-----|
| **Dashboard** | Project → CDN → Routing tab. Live map of global traffic, cache management, and route editor in one view. |
| **REST API** | `GET/POST/PATCH/DELETE /v1/projects/{projectId}/routes` — 8 dedicated endpoints for CRUD on project routes. |
| **Vercel CLI** | Use `vercel routes` to stage, inspect, publish, restore, and export project-level rules. |

Deployment-level routes in `vercel.json`, `vercel.ts`, or framework config are a separate mechanism (row 3 above) and require a deploy.

Use project-level routes for operational changes (CORS headers, API proxy rewrites, A/B redirects) that shouldn't require a full redeploy.

## Programmatic Configuration with `vercel.ts`

Instead of static `vercel.json`, you can use `vercel.ts` (or `.js`, `.mjs`, `.cjs`, `.mts`) with the `@vercel/config` package for type-safe, dynamic routing configuration:

```ts
// vercel.ts
import { routes, type VercelConfig } from '@vercel/config/v1';

export const config: VercelConfig = {
  rewrites: [
    routes.rewrite('/api/(.*)', 'https://backend.example.com/$1'),
  ],
  headers: [
    routes.header('/(.*)', [{ key: 'X-Frame-Options', value: 'DENY' }]),
  ],
};
```

For project-level rules that take effect without a deployment, use `vercel routes add`, inspect staged changes with `vercel routes list --diff`, then run `vercel routes publish`.

**Constraint**: Only one config file per project — `vercel.json` or `vercel.ts`, not both.

## When to Use

- Geo-personalization of static pages (runs before cache)
- A/B testing rewrites with Global Config
- Custom redirects based on request properties
- Header injection (CSP, CORS, custom headers)
- Lightweight auth checks (defense-in-depth only — not sole auth layer)
- Project-level routes for headers/rewrites without redeploying

## When NOT to Use

- Need full Node.js APIs in Next.js → use `proxy.ts`
- General compute or request handling → use Vercel Functions on the default Node.js runtime
- Heavy business logic or database queries → use server-side framework features
- Auth as sole protection → use Layouts, Server Components, or Route Handlers
- Thousands of static redirects → use Bulk Redirects (up to 1M per project)

## References

- 📖 docs: https://vercel.com/docs/routing-middleware
- 📖 API reference: https://vercel.com/docs/routing-middleware/api
- 📖 getting started: https://vercel.com/docs/routing-middleware/getting-started
