---
title: "Next.js: Middleware (proxy.ts)"
slug: "nextjs-middleware"
category: "frontend"
tags: ["frontend", "nextjs", "middleware", "proxy", "auth"]
status: "stable"
last_updated: 2026-08-14
summary: "Next.js 16 proxy.ts (formerly middleware.ts): matcher config, Node-only runtime, rewrite vs redirect vs next, cookies and headers, and per-request cost."
related: ["[[frontend/nextjs]]", "[[frontend/nextjs-app-router]]", "[[frontend/nextjs-route-handlers]]", "[[frontend/nextjs-server-actions]]", "[[frontend/nextjs-caching]]", "[[ops/vercel]]", "[[ops/cloudflare]]"]
---

## Overview

A `proxy.ts` at the project root runs before the matched request reaches a route. Next.js 16 renamed this file from `middleware.ts` to `proxy.ts` to make its job clearer: it sits between the request and the app, proxying and rewriting before a route ever runs. Use it for cross-cutting work that has to happen on every request: auth gating, locale routing, A/B redirects, header injection. Unlike the old middleware, `proxy.ts` runs on the Node.js runtime only; Edge is no longer an option here. The cost still compounds because the function fires on every matched request, so the matcher and the runtime rules below still apply.

## Place `proxy.ts` at the project root

The file lives at the project root, not inside `app/` or `pages/`. There is one proxy file per project. It exports a named `proxy` function that accepts a `NextRequest` and returns a `NextResponse`.

```ts
// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function proxy(req: NextRequest) {
  return NextResponse.next();
}
```

A return value of `NextResponse.next()` lets the request proceed. Any other response short-circuits the pipeline; the route handler never runs.

`middleware.ts` still works in Next.js 16 for backward compatibility, but it is deprecated and slated for removal; migrate by renaming the file to `proxy.ts` and the exported function to `proxy`. Do not keep both files in the same project; Next.js only recognizes one.

## Constrain the matcher; do not run on every asset

The default matcher runs on every request, including static assets. That is wasteful and expensive. Constrain it.

```ts
export const config = {
  matcher: [
    // Skip Next internals and static files; match everything else.
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)).*)",
  ],
};
```

For specific routes, list them: `matcher: ["/dashboard/:path*", "/admin/:path*"]`. The matcher is the cheapest filter; pruning here pays for itself on every page load.

## Pick `redirect`, `rewrite`, or `next` deliberately

Three primitives cover most proxy logic:

- `NextResponse.redirect(url, status)`: tell the browser to navigate elsewhere. The URL changes; the user sees a 30x.
- `NextResponse.rewrite(url)`: serve a different route under the original URL. The browser does not see the rewrite; the user keeps the URL they typed.
- `NextResponse.next()`: continue to the matched route, optionally with mutated request headers.

```ts
export function proxy(req: NextRequest) {
  const country = req.geo?.country ?? "US";
  if (req.nextUrl.pathname === "/") {
    return NextResponse.rewrite(new URL(`/landing/${country.toLowerCase()}`, req.url));
  }
  return NextResponse.next();
}
```

Use rewrites for A/B routing and locale serving. Use redirects for canonical URL enforcement and moved pages. Use `next` when you only need to mutate headers or cookies.

## Read and write cookies on `req` and the response

Cookies live on `req.cookies` for reading and on the response for writing. Set a cookie in the response that goes back to the browser.

```ts
const res = NextResponse.next();
const sid = req.cookies.get("sid")?.value;
if (!sid) {
  res.cookies.set("sid", crypto.randomUUID(), {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
  });
}
return res;
```

Always set `httpOnly`, `secure`, and an explicit `sameSite` on auth cookies. The proxy is the right place to rotate session tokens, because it runs before the page reads them.

## Forward signal via request headers, not globals

To pass data from the proxy to a page or handler, set request headers on the response and read them server-side in the route.

```ts
const res = NextResponse.next({
  request: { headers: new Headers({ ...req.headers, "x-user-id": userId }) },
});
return res;
```

In the route, read with `headers()` from `next/headers`. Do not try to share state in module scope; each invocation is isolated.

## `proxy.ts` runs on Node.js only; there is no runtime switch

Node.js runtime is not configurable for `proxy.ts`; it always runs on Node.js. This is the biggest behavioral change from the old middleware, which defaulted to the Edge runtime (V8 isolates, Web APIs only, no `node:*` modules) and let you opt into Node with `export const config = { runtime: "nodejs" }`. That config option is gone: a Next.js 16 proxy file has full Node API access by default, at the cost of Edge's sub-50ms cold starts and global execution.

If your deployment target relies on Edge-only proxy execution for latency, that trade-off no longer exists in Next.js 16; budget for Node cold starts instead, or push latency-sensitive checks (auth token verification, geolocation) to a CDN-level rule outside Next.js.

## Treat the proxy as a hot path

The proxy runs on every matched request. A 50ms proxy function adds 50ms to every page in the matcher. Budget accordingly.

- Cache token-verification results in a cookie or a fast KV. Do not call the auth provider on every request.
- Skip JSON parsing of large request bodies; reroute the work to the route handler.
- Avoid calling external APIs unless the request truly needs the call.
- Profile cold and warm invocations on the host (Vercel surfaces this; see [[ops/vercel]]).

For per-request work that is not cross-cutting, move it to the route. For cross-cutting work that is too expensive to run on every request, push it to the route and let the proxy do only the matcher and the redirect.

## Related

- [[frontend/nextjs]]
- [[frontend/nextjs-app-router]]
- [[frontend/nextjs-route-handlers]]
- [[frontend/nextjs-server-actions]]
- [[frontend/nextjs-caching]]
- [[ops/vercel]]
- [[ops/cloudflare]]
