Next.js Proxy and Middleware
Run code before a request completes with proxy.ts, the Next.js 16 replacement for middleware.
TL;DR
- Create
proxy.tsat the project root and export aproxyfunction. - Limit where it runs with the
matcheroption inconfig. - Return
NextResponse.redirect()orNextResponse.rewrite()to change the request.
File and Matcher
proxy.tsExport a function named proxy that runs before matched requests complete.
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
return NextResponse.next();
}matcherLimit the proxy to specific paths by exporting a config object.
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};Negative MatcherSkip API routes and static assets with a pattern that excludes them.
export const config = {
matcher: [
'/((?!api|_next/static|_next/image).*)',
],
};middleware-to-proxyRename an existing middleware.ts file and its function with the official codemod.
npx @next/codemod@canary middleware-to-proxy .Redirects and Rewrites
NextResponse.redirect()Send the browser to a different URL, which changes the address bar.
if (request.nextUrl.pathname === '/old') {
return NextResponse.redirect(
new URL('/new', request.url)
);
}NextResponse.rewrite()Serve a different route internally while the visible URL stays the same.
if (request.nextUrl.pathname.startsWith('/docs')) {
return NextResponse.rewrite(
new URL('/help', request.url)
);
}Header RedirectRedirect based on a request header, such as the browser's preferred language.
const lang = request.headers
.get('accept-language')
?.slice(0, 2);
if (lang === 'es') {
return NextResponse.redirect(
new URL('/es', request.url)
);
}Headers and Cookies
Response HeadersAdd a security header to every matched response in one place.
const response = NextResponse.next();
response.headers.set('X-Frame-Options', 'DENY');
return response;Request HeadersPass a value to your pages by adding a header to the forwarded request.
const headers = new Headers(request.headers);
headers.set('x-pathname', request.nextUrl.pathname);
return NextResponse.next({
request: { headers },
});CookiesRead cookies from the request and set new ones on the response.
const theme = request.cookies.get('theme')?.value;
const response = NextResponse.next();
response.cookies.set('visited', 'true');
return response;Auth Checks
Session RedirectRedirect to the login page when the session cookie is missing.
export function proxy(request: NextRequest) {
const session = request.cookies.get('session');
if (!session) {
return NextResponse.redirect(
new URL('/login', request.url)
);
}
return NextResponse.next();
}Direct ResponseReject an API request with a JSON error instead of a redirect.
if (!request.headers.get('authorization')) {
return Response.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}waitUntil()Finish background work such as logging after the response is sent.
import type { NextFetchEvent } from 'next/server';
export function proxy(
request: NextRequest,
event: NextFetchEvent
) {
event.waitUntil(logVisit(request.nextUrl.pathname));
return NextResponse.next();
}Tips
- Run
npx @next/codemod@canary middleware-to-proxy .to rename the file and the exported function automatically when you upgrade. - Exclude static files with a negative
matcherpattern, so the proxy does not run on every image and script request.
Warnings
- The
proxy.tsfile always uses the Node.js runtime, and setting aruntimeoption in it throws an error. - Never treat
proxy.tsas your only auth check, because a matcher change can silently leave Server Functions unprotected.
In Practice
Redirect visitors without a session cookie away from the dashboard, and remember where they were going.
- The matcher limits the proxy to dashboard paths, so other pages are untouched.
- The cookie check is fast, because it reads the request without a database call.
- The from parameter lets the login page send the user back afterward.
- Pages must still verify the session, since this check is only a first filter.
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const session = request.cookies.get('session')?.value;
if (!session) {
const login = new URL('/login', request.url);
login.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(login);
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};FAQ
It was renamed. middleware.ts is now proxy.ts, and the exported function is proxy instead of middleware. The old file name still works but is deprecated.
Run npx @next/codemod@canary middleware-to-proxy . in your project. It renames the file and the function for you. The rest of your code, including config.matcher, stays the same.
Place proxy.ts in the project root, next to the app folder. If you use a src folder, put it inside src. Only one proxy file is supported per project.
Use it for a quick check, such as redirecting when a session cookie is missing. Always verify the user again in your pages, Server Functions, and route handlers.