Next.js 16 renamed the middleware file convention to proxy. The mechanism is mostly the same. The naming change is intentional: it reframes the file as a request transform that sits in front of your app rather than as application middleware. The new name avoids implying that it should run shared application logic.
This is what changed, what stayed the same, and the four patterns that work well in proxy.ts for an agent-ready site.
What changed
- File name:
middleware.ts→proxy.ts. - Function name:
middleware→proxy(or default export). - Mental model: the file runs before routes render and may be deployed near the network boundary in optimized cases. It should not depend on shared in-process state.
- The matcher config object stays the same shape.
The function signature and NextRequest/NextResponse APIs are unchanged.
Migration
If you upgraded from an earlier Next, the migration is a rename:
// Before: middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
return NextResponse.next();
}
export const config = { matcher: ["/dashboard/:path*"] };// After: proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
return NextResponse.next();
}
export const config = { matcher: ["/dashboard/:path*"] };That is the full migration for most projects.
When to use proxy.ts
Three things proxy is good at:
- Setting or rewriting headers on the request or response.
- Rewriting the URL based on something about the request.
- Returning an early response to skip the route entirely (auth gates, geo blocks).
Three things proxy is bad at:
- Anything that needs database access or shared state.
- Long-running logic that would slow every request.
- Application logic that belongs in a route handler or layout.
If your proxy is calling fetch() multiple times or running heavy parsing, it is doing too much. Move that into the route.
Pattern 1: Link headers for agent discovery
Send a Link response header on the homepage so agents can discover machine-readable resources without HTML parsing.
// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const LINK_HEADER =
'</.well-known/api-catalog>; rel="api-catalog", ' +
'</llms.txt>; rel="describedby"; type="text/plain", ' +
'</sitemap.xml>; rel="sitemap"; type="application/xml", ' +
'</rss>; rel="alternate"; type="application/rss+xml"';
export function proxy(request: NextRequest) {
if (request.nextUrl.pathname !== "/") {
return NextResponse.next();
}
const response = NextResponse.next();
response.headers.set("Link", LINK_HEADER);
return response;
}
export const config = { matcher: ["/"] };Build the right header value with the Link Headers Builder.
Pattern 2: Markdown content negotiation
When an agent sends Accept: text/markdown, rewrite to a markdown route handler.
function acceptsMarkdown(request: NextRequest) {
const accept = request.headers.get("accept") ?? "";
return accept.toLowerCase().includes("text/markdown");
}
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === "/" && acceptsMarkdown(request)) {
const url = request.nextUrl.clone();
url.pathname = "/markdown-home";
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
export const config = { matcher: ["/"] };See the content negotiation guide for the matching markdown route.
Pattern 3: Composing multiple concerns
When you have more than one concern, the cleanest pattern is small functions composed in order, each returning either a response or a "continue" signal.
export function proxy(request: NextRequest) {
const acceptResponse = handleMarkdownNegotiation(request);
if (acceptResponse) return acceptResponse;
const authResponse = handleAuthGate(request);
if (authResponse) return authResponse;
const response = NextResponse.next();
setLinkHeader(request, response);
return response;
}Avoid putting everything in a single 200-line function. Refactor early.
Pattern 4: AI bot filtering in proxy
When you want to apply different policies to AI bots than to human users, do the cheap classification once at the request boundary.
function classifyBot(ua: string): "ai" | "search" | "browser" | "unknown" {
if (/GPTBot|ClaudeBot|PerplexityBot|Google-Extended/i.test(ua)) return "ai";
if (/Googlebot|Bingbot/i.test(ua)) return "search";
if (/Mozilla.*Chrome|Safari|Firefox/i.test(ua)) return "browser";
return "unknown";
}
export function proxy(request: NextRequest) {
const ua = request.headers.get("user-agent") ?? "";
const kind = classifyBot(ua);
// Annotate downstream handlers
const response = NextResponse.next({
request: {
headers: new Headers([...request.headers, ["x-bot-class", kind]]),
},
});
return response;
}The x-bot-class header is now available to your route handlers and pages, which can serve different content (or a stripped markdown variant) without re-classifying.
For verification, see Verify the bot is real. Proxy can consume a cached verification signal, but do not make every request wait on DNS.
Avoid these mistakes
Setting cache headers from proxy
Cache headers belong to the route handler that produced the response. Setting them in proxy fights against framework caching.
Reading the request body
Proxy should only inspect headers and the URL. Reading the body is expensive at the request boundary and disables some optimizations. If you need the body, do the work in the route.
Doing slow verification inline
A reverse DNS lookup in proxy will get hit on every matching request. Prefer a CDN rule, route handler, or cached verification service. If proxy must participate, treat the cache as soft and avoid blocking normal traffic on a single lookup failure.
The matcher
Two patterns matter.
Match a single path
export const config = { matcher: ["/"] };Match all paths except static assets
export const config = {
matcher: [
/*
* Match all paths except:
* - _next/static, _next/image, favicon, opengraph-image,
* - any file that has an extension in the URL
*/
"/((?!_next/static|_next/image|favicon.ico|opengraph-image|.*\\.).*)",
],
};For most agent-readiness use cases, the homepage matcher is enough.
What we ship at AgentScan
This site uses proxy.ts for two things: setting the homepage Link header (advertising api-catalog, llms.txt, sitemap, rss, atom) and the markdown content negotiation that returns text/markdown when an agent asks. Both are visible to agents on the very first request without HTML parsing.
The full file is roughly 30 lines. Everything else lives in route handlers.
Why this matters
proxy.ts is the request boundary in front of your app. In 2026 it is also a natural place to participate in the agentic web: send the discovery headers, do the content negotiation, and classify bots. The rename clarifies the role and keeps the file small. Use it for those concerns and resist the temptation to grow it.
