Next.js 16 replaced middleware.ts with proxy.ts

In short

In Next.js 16, middleware.ts is gone. The replacement is proxy.ts at the project root, exporting a function named proxy(request) and a config.matcher, and it runs on the Node runtime rather than Edge. The matcher is read statically at build time, so it cannot reference an imported constant.

A signpost with the old nameplate unscrewed and leaning against the post as a new one is fixed on.
Old
middleware.ts, export default middleware(), Edge runtime
New
proxy.ts, export function proxy(request), Node runtime
Export name
Must be `proxy` — a default export is not picked up
config.matcher
Read statically at build; cannot be a computed value

If you are upgrading to Next.js 16 and your middleware.ts has quietly stopped running, it is not broken — it is no longer the file Next looks for. The concept survives under a new name, a new export, and a different runtime.

#What actually changed

BeforeNext.js 16
Filemiddleware.tsproxy.ts
Exportdefault, or named middlewarenamed proxy
Signature(request: NextRequest)(request: NextRequest)
RuntimeEdgeNode
Configexport const configexport const config

The rename is the visible part. The runtime move is the part that changes what you can write: on Node you are no longer confined to the Edge subset, so Node built-ins and libraries that depend on them work in a file where they previously could not.

#The shape of the new file

import { NextResponse, type NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  const res = NextResponse.next();
  res.headers.set("X-Robots-Tag", "noindex, nofollow, noarchive");
  return res;
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
proxy.ts, at the project root — the same level middleware.ts sat at.

#The matcher is read statically

This is the one that cost me real time. config.matcher is not evaluated at runtime — Next reads it out of the source at build time, before any module graph exists. That means it cannot reference anything imported.

// ✗ Does not work. ADMIN_PATH is an import; the build-time reader
//   cannot resolve it, and the matcher ends up wrong or empty.
import { ADMIN_PATH } from "@/lib/admin-path";
export const config = { matcher: [`${ADMIN_PATH}/:path*`] };

// ✓ Works. The literal has to be present in this file.
export const config = { matcher: ["/desk-4f7a/:path*"] };

If you keep a path constant in a shared module — which you should, if several files need it — the matcher becomes a deliberate duplication of that value. Comment it as such at both ends, because it looks exactly like the kind of duplication a future reader will helpfully refactor away.

#Migrating in practice

  1. Rename middleware.ts to proxy.ts, keeping it at the project root.
  2. Rename the exported function to proxy and make sure it is a named export.
  3. Inline any imported constants used inside config.matcher as string literals.
  4. Re-check anything that assumed the Edge runtime — the file now runs on Node.
  5. Verify against a production build, not next dev.

#Why the rename is the right call

"Middleware" was borrowed from Express, where it means a chain of handlers each calling the next. Next's version was never that. It was one function, running once, in front of routing — closer to a reverse proxy than to a middleware stack, and the name led people to expect composition that was never there.

proxy describes what the file actually is. Renames are annoying exactly once; a misleading name is annoying every time somebody new reads the code.

Questions this answers

What replaced middleware.ts in Next.js 16?

proxy.ts, placed at the project root. It exports a named function `proxy(request)` instead of a default or `middleware` export, and it runs on the Node runtime rather than the Edge runtime. The `config.matcher` export works the same way.

Why is my Next.js 16 proxy.ts not running?

The most common causes are a default export instead of the required named `proxy` export, or a config.matcher that references an imported constant. The matcher is read statically at build time, so it can only contain string literals present in that file. Neither mistake produces an error.

Can config.matcher use a variable in Next.js 16?

No. Next reads config.matcher out of the source at build time, before the module graph is resolved, so imported constants and computed values cannot be used. The matcher patterns must be string literals written directly in proxy.ts.

See also

  • Learnings The skill map — knowledge domains, competencies and credentials

Let's Build
What Comes Next.

Open to meaningful collaborations, AI-native systems, product strategy, and future-focused conversations.

“Human instinct. AI amplification.
Systemic execution.”

Suman Debnath

·

Brand Marketing Leader & AI Product Builder

© 2026

This site records visit data — pages viewed, time and scroll depth, device, your IP address and the approximate location and network provider derived from it — and sends it to me privately. It also runs Google Analytics and Vercel Analytics. Full detail and how to opt out.

This site, its code and its content are © 2026 Suman Debnath. All rights reserved — none of it is open source, and copying it needs permission first. Terms of use.