· via dev.to (home feed)
Next.js 16 renames middleware to proxy, deprecating the old file convention
Next.js 16 deprecates middleware.ts in favour of a proxy.ts file exporting a proxy function. The rename is easy, but the matcher config decides which requests the function touches at all.

Middleware becomes proxy
Next.js 16 deprecates the long-standing middleware file convention and replaces it with a file named proxy.ts exporting a function called proxy. According to a post on dev.to, the framework's own documentation states the change plainly: the middleware convention is deprecated and has been given the new name. For teams upgrading, the mechanical part of the migration is minimal — the author reports that an app running 16.2.4 needed little more than a rename of the file and its export.
The meaningful work, the post argues, is not the rename but the config object underneath the export, which determines which requests the function ever sees.
The matcher is a correctness decision
The Next.js documentation, as cited in the post, is clear about the default behaviour: without a matcher, the proxy runs on every request, including framework assets under _next/static and _next/image and everything served from the public directory. That turns the matcher from an optional performance tweak into a core design decision.
The post's own matcher is a single negative lookahead excluding four categories of path: the framework's asset routes, the favicon.ico path that browsers request automatically, the app's webhook endpoints, and any path ending in a common image extension.
The anchor on that last branch is deliberate. A suffix pattern without the end-of-string anchor would also exclude paths that merely contain ".svg" somewhere — a URL with a query string such as ref=logo.svg would silently stop receiving session refreshes. Anchoring the match keeps the rule about which file is being requested rather than which string appears anywhere in the URL.
Why the webhooks are left out
Everything else under the app's /api routes is called by its own frontend with a token or cookie; the webhook routes are called by Stripe and Resend. The post gives three reasons for excluding them. No browser is involved, so CORS headers on those responses serve no purpose. No user session exists to refresh, so running the session refresher would build a Supabase client and read cookies that are not there on every delivery and retry. And authentication for these routes is an HMAC signature over the raw body, which belongs in the route handler rather than in a function that runs before it.
The exclusion is observable from a browser: a cross-origin fetch to an ordinary API route resolves with a readable status, while the same fetch to a webhook path fails with a network error because nothing on that path answers the preflight. One subtlety the post highlights is that the response object shows the allow-origin header as null — that header is never exposed to JavaScript, and the proof it was present is that the request resolved at all.
Ordering and failing open
The order of operations inside the function matters more than it appears. A preflight OPTIONS request is a policy question with no cookies worth rotating, so it returns immediately, built from headers alone, and never constructs an auth client. If the session refresh ran first, every browser API call would pay for two refreshes instead of one.
The session refresh itself is wrapped so that a Supabase outage or a misconfigured environment variable refreshes nothing rather than failing every matched request — which would effectively take the site down. The author is careful about what this fails open on: refreshing a session, not authorising one. Routes that need a user still resolve that user themselves and return 401 otherwise.
The edge deployment caveat
The documentation warns that proxy is meant to run separately from render code and, in optimised setups, may be deployed to the CDN — and advises against relying on shared modules or globals. The post's proxy imports two local modules, one of which parses its allowed-origin set from environment variables once at module initialisation. That sits on the safe side of the warning, but only because the state is read-only and identical in every instance. A counter, rate-limit bucket or memo in that scope would be shared across potentially dozens of isolated edge instances and would produce intermittent, hard-to-reproduce bugs.
Why it matters
Every Next.js codebase using middleware.ts has to make this rename when moving to 16, and while the change itself is mechanical, it forces a rare review of code that runs before every matched request. Mistakes in that layer are systemic: an overly broad matcher wastes work on static assets, an unanchored pattern silently skips work on legitimate paths, and a throwing helper can take down pages across an entire site. The proxy's positioning toward separate, possibly CDN-edge deployment also raises the bar for statelessness — assumptions that held when middleware ran alongside render code may not hold where it runs in isolated instances.
- #next-js
- #react
- #middleware
- #web-development
- #javascript