proxy.js

Runs server code before a request is routed, to rewrite, redirect or modify headers and cookies.

Signature
export function proxy(
  request: NextRequest,
  event: NextFetchEvent
): NextResponse | Response | Promise<NextResponse | Response> | void

export const config: { matcher: string | string[] | Array<{ source: string; locale?: boolean; has?: object[]; missing?: object[] }> }
  • Added in Next.js v12.0.0
  • Deprecated since PHP v16.0.0 — the middleware.js filename and middleware() export are deprecated in favour of proxy.js and proxy().

Parameters

ParameterTypeDescription
$requestoptionalNextRequestThe incoming request, extending the Web Request API with request.cookies and the parsed request.nextUrl.
$eventoptionalNextFetchEventExposes waitUntil(promise), which keeps the invocation alive until the promise settles so background work such as logging can finish after the response is sent.
$config.matcheroptionalstring | string[] | Array<{ source, locale?, has?, missing? }>= undefined (runs on every request)Paths this proxy runs on. Values must be statically analysable constants. Without it, the proxy runs on every request including _next/static, _next/image and public/ assets.

Return value

NextResponse | Response | Promise<NextResponse | Response> | voidA NextResponse or Response to redirect, rewrite or answer the request directly. Returning nothing lets the request continue to normal routing.

Errors

Setting the runtime segment config option in a proxy file throws — the option is unavailable there. Only one proxy function per file is supported.

Changelog

  • PHP v16.0.0Middleware is deprecated and renamed to Proxy, and Proxy now defaults to the Node.js runtime. Migrate with: npx @next/codemod@canary middleware-to-proxy .
  • PHP v15.5.0Middleware can use the Node.js runtime (stable).
  • PHP v15.2.0Middleware can use the Node.js runtime (experimental).
  • PHP v13.1.0Advanced flags added: skipProxyUrlNormalize (formerly skipMiddlewareUrlNormalize) and skipTrailingSlashRedirect. Responding directly from the proxy became possible.
  • PHP v13.0.0Can modify request headers, response headers and send responses.
  • PHP v12.2.0Middleware is stable.

proxy.ts runs on the server before a request is routed. Return a NextResponse to redirect, rewrite, set headers or cookies, or answer outright; return nothing to let the request through. It was called middleware.js until v16 — run npx @next/codemod@canary middleware-to-proxy . to rename the file and the export. The name change is a signal: this is a network boundary in front of your app, sometimes deployed to a CDN, and Vercel now recommends it as a last resort rather than a default tool.

Place it at the project root, beside app or pages (or inside src).

What trips people up

  • Never treat it as your auth boundary. Server Functions are POST requests to the route that uses them, so a matcher excluding a path also skips them — and moving a function elsewhere silently drops coverage. Verify auth inside every Server Function.
  • No matcher means every request, including _next/static and public/. A redirect written without one can block your own CSS.
  • Matchers must be literal constants — a variable is ignored at build time.
  • _next/data runs anyway, even when excluded. Deliberate, so a protected page's data route is not left open.
  • Defaults to Node.js in 16, and setting runtime here throws.
  • Share nothing through globals; pass state via headers, cookies or the URL.

Example

Example · typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// This function can be marked `async` if using `await` inside
export function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

export const config = {
  matcher: '/about/:path*',
}

When to use it

  • Redirect signed-out visitors away from /dashboard before any rendering starts
  • Rewrite a country prefix onto incoming URLs for localised routing
  • Attach a request ID or feature-flag header that Server Components can read
  • Answer unauthenticated /api requests with a 401 JSON body directly

Frequently asked questions

What replaced middleware.js in Next.js 16?

It was renamed to proxy.js. The file becomes proxy.ts or proxy.js at the project root and the exported function is named proxy instead of middleware; a default export also works. Run npx @next/codemod@canary middleware-to-proxy . to rename both automatically. The old middleware convention is deprecated, and the rename reflects that this runs as a network boundary in front of the app rather than as Express-style middleware.

Why does my proxy run on images and CSS files?

Because without a config.matcher the proxy runs on every request, including _next/static, _next/image and everything in public/. An auth redirect written that way will block your own assets and the app renders unstyled. Add a negative matcher such as '/((?!api|_next/static|_next/image|favicon.ico).*)', and remember matcher values must be literal constants — variables are ignored at build time.

Is it safe to do authentication in proxy.js?

Not on its own. Server Functions are handled as POST requests to the route where they are used, so a matcher that excludes a path also skips the Server Functions on it, and refactoring a function to a different route can silently remove coverage. Treat proxy checks as an optimization for redirecting unauthenticated page visits, and verify authentication and authorization inside every Server Function and Data Access Layer call.

More examples

Excluding static assets from the matcher

Without a matcher the proxy runs on literally every request, so an auth redirect would bounce your own stylesheets and images too — the usual cause of an app that renders unstyled behind a login wall. This negative lookahead is the standard exclusion list. Note that _next/data routes still run the proxy even when excluded, deliberately, so a protected page's data route cannot be left open by accident.

Example · typescript
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

Passing information into the app via headers

The distinction matters: NextResponse.next({ request: { headers } }) makes the header visible upstream to your Server Components via headers(), while headers set on the response object go to the browser. Writing NextResponse.next({ headers }) instead sends them to the client, which is the mistake behind a header that your components can never read. Keep them small — large headers can trigger a 431.

Example · typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-proxy1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-proxy1', 'hello')

  // You can also set request headers in NextResponse.next
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  })

  // Set a new response header `x-hello-from-proxy2`
  response.headers.set('x-hello-from-proxy2', 'hello')
  return response
}

Answering the request directly, and why it is not enough

Returning a Response short-circuits routing; returning nothing lets the request continue. This is a fine cheap gate for /api, but it is not a security boundary: Server Functions are POST requests to the route that hosts them, so this matcher never sees a Server Function used on a page outside /api, and moving one between routes changes its coverage silently. Re-check authentication inside each Server Function.

Example · typescript
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'

// Limit the proxy to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}

export function proxy(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

Discussion

  • Be the first to comment on this lesson.