NextResponse

Extends the Web Response with next(), redirect(), rewrite(), json() and a cookies helper.

Signature
class NextResponse extends Response

Parameters

ParameterTypeDescription
$NextResponse.json(body, init?)staticProduce a response with the given JSON body. init takes the usual ResponseInit fields such as status and headers.
$NextResponse.redirect(url, init?)staticProduce a response that redirects to a URL. The URL can be built and modified first, typically from request.nextUrl.
$NextResponse.rewrite(url, init?)staticProduce a response that rewrites (proxies) the given URL while the browser keeps showing the original one.
$NextResponse.next(init?)optionalstaticReturn early and continue routing. Pass { request: { headers } } to forward modified request headers upstream.
$cookiesResponseCookiesRead or mutate the response's Set-Cookie header: set(name, value), get(name), getAll(name?), has(name), delete(name).

Return value

A Response you return from proxy() or a Route Handler, carrying Next.js routing semantics.

NextResponse is the Web Response with the four routing decisions a proxy can make bolted on: continue (next()), send the browser elsewhere (redirect()), serve a different route under the same URL (rewrite()), or reply directly (json()). Its cookies jar is where you actually set cookies in the browser from a proxy.

Choosing between them

redirect() changes the address bar and costs a round trip; rewrite() does not and is what you want for A/B splits, locale routing or device variants. next() is the pass-through — return it whenever the proxy has nothing to change, and attach cookies or headers to it first if you do.

What trips people up

  • NextResponse.next({ request: { headers } }) forwards headers upstream to the page or handler and hides them from the client. NextResponse.next({ headers }) is different — it sends them to the browser, and overriding things like Content-Type that way breaks Server Actions and streaming.
  • Do not copy all incoming headers forward. Build an allow-list; blanket copying leaks cookie and authorization to upstream services.
  • redirect() needs an absolute URL — construct it with new URL(path, request.url).

Example

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

export function proxy(request: NextRequest) {
  const session = request.cookies.get('session-id')

  if (!session) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', request.nextUrl.pathname)
    return NextResponse.redirect(loginUrl)
  }

  const response = NextResponse.next()
  response.cookies.set('last-seen-path', request.nextUrl.pathname, {
    path: '/',
    sameSite: 'lax',
  })

  return response
}

When to use it

  • Redirect unauthenticated users to a login page and remember where they came from
  • Rewrite an A/B test variant without changing the visible URL
  • Set or clear a cookie from proxy.js while letting the request continue
  • Return a JSON error with an explicit status code from a Route Handler

Frequently asked questions

How do I redirect from middleware or proxy.js?

Return NextResponse.redirect() with an absolute URL, built as new URL('/login', request.url). Relative strings are rejected. Use rewrite() instead if you want to serve different content without changing what the address bar shows.

What is the difference between rewrite and redirect?

redirect() sends the browser a redirect response so the URL visibly changes and a second request follows, while rewrite() proxies a different route on the server with the original URL still shown. Rewrites suit A/B tests, locale routing and device variants.

How do I pass data from proxy.js to my page?

Use NextResponse.next({ request: { headers } }) with a modified Headers object; the page reads it through await headers() and the client never sees it. Avoid copying every incoming header forward — build an allow-list, since blanket copying can leak cookie and authorization headers.

More examples

Forwarding a header upstream, safely

The page or Route Handler downstream can now read x-version through await headers(), and the client never sees it. The nesting matters: { request: { headers } } goes upstream, whereas { headers } at the top level sends them to the browser — a shorthand the docs explicitly warn against, since overriding Content-Type there breaks Server Actions and streaming.

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

export function proxy(request: NextRequest) {
  // Given an incoming request...
  const newHeaders = new Headers(request.headers)
  // Add a new header
  newHeaders.set('x-version', '123')

  // Forward the modified request headers upstream
  return NextResponse.next({
    request: {
      headers: newHeaders,
    },
  })
}

Rewriting a variant behind one URL

Both cohorts share one public URL while Next.js renders two different routes, so each variant keeps its own cache entry and its own build output. A redirect() here would flash a second URL in the address bar and reveal the experiment to the visitor.

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

export function proxy(request: NextRequest) {
  const bucket = request.cookies.get('bucket')?.value

  if (bucket === 'variant') {
    // Incoming request: /pricing, browser shows /pricing
    // Rewritten request: /pricing-variant, browser shows /pricing
    return NextResponse.rewrite(new URL('/pricing-variant', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/pricing',
}

JSON responses with explicit status

json() sets Content-Type and serialises the body for you; the second argument is a normal ResponseInit, which is how the 400 is applied. Setting the cookie on the returned response rather than on the request is what actually emits a Set-Cookie header to the browser.

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

export async function POST(request: Request) {
  const body = await request.json().catch(() => null)

  if (!body?.email) {
    return NextResponse.json(
      { error: 'email is required' },
      { status: 400 }
    )
  }

  const response = NextResponse.json({ ok: true })
  response.cookies.set('signed-up', '1', { path: '/', httpOnly: true })

  return response
}

Discussion

  • Be the first to comment on this lesson.