Type something to search...
Next.js NextResponse

Next.js NextResponse

If you've written a Route Handler or a proxy file in Next.js, you've almost certainly returned a NextResponse without stopping to think about what it actually is. It looks like a Response, it behaves like a Response, and for the most part you can treat it like one. But NextResponse is a subclass that bolts on a handful of framework-specific conveniences — cookie helpers, and static constructors for redirects, rewrites, and pass-through routing — that the standard Web Response object simply doesn't have.

Most developers pick these methods up by copying a snippet from a tutorial and never look at the full surface area. That's fine for a while, but it starts causing real bugs the moment you need to do something slightly off the beaten path: forwarding a modified request header upstream, deleting a cookie conditionally, or debugging why a redirect isn't preserving query parameters. This article is the reference I wish I'd had the first time I needed to do any of those things.

Why NextResponse Exists At All

Route Handlers and proxy files in Next.js are built on the standard Web Request/Response APIs — the same Request and Response constructors you'd find in a Cloudflare Worker or a browser's Service Worker. That's a deliberate design choice: it means your routing logic isn't locked to a Node.js-specific API shape, and it can run on the Edge runtime without any translation layer.

The problem is that the plain Web Response API is too general-purpose for the things Next.js routing needs to do constantly — redirect to a new URL, rewrite a request to a different internal path without changing what the browser shows, or simply pass a request through unchanged while attaching a header. You can do all of this with the raw Response API, but it takes several lines of boilerplate every time. NextResponse exists to collapse that boilerplate into single method calls, while still being a fully compliant Response under the hood — so anything you already know about Response (status codes, headers, streaming bodies) still applies.

The cookies Property

Every NextResponse instance exposes a cookies object that reads and writes the response's Set-Cookie header without you having to construct that header string by hand. This matters more than it looks like at first glance, because Set-Cookie has fussy formatting rules (attributes separated by semicolons, values that need escaping, and the fact that a response can carry multiple Set-Cookie headers simultaneously, one per cookie). Hand-rolling this is exactly the kind of thing that works in your quick test and then breaks on a cookie value containing a comma.

Setting a cookie

// Given incoming request /home
let response = NextResponse.next();
// Set a cookie to hide the banner
response.cookies.set("show-banner", "false");
// Response will have a `Set-Cookie: show-banner=false; path=/home` header
return response;

The two-argument form (name, value) is the common case. cookies.set also accepts a third options object for the attributes you'd expect — maxAge, expires, path, domain, secure, httpOnly, and sameSite. If you omit them, Next.js falls back to sensible defaults, but for anything security-sensitive (auth tokens, session identifiers) you should be setting httpOnly: true and secure: true explicitly rather than relying on defaults you haven't verified.

Reading a cookie back off the response you're building

// Given incoming request /home
let response = NextResponse.next();
// { name: 'show-banner', value: 'false', Path: '/home' }
response.cookies.get("show-banner");

This is a subtle but important distinction: response.cookies.get() reads cookies you've already staged onto this response object — it does not read the cookies the browser sent in on the original request. If you need to inspect incoming cookies (say, to check whether a user already has a session before deciding whether to set a new one), you read those from request.cookies, a separate object entirely, not from the response you're constructing. Mixing these two up is one of the most common mistakes people make with the Next.js request/response cookie APIs — reaching for response.cookies.get() when they actually needed request.cookies.get(), and getting undefined back because the response hasn't had that cookie set on it yet.

If a cookie name has been set multiple times (which can legitimately happen — some experimentation frameworks stack multiple values under the same key with different paths), get() returns only the first match. If you need every value, use getAll():

// [
//   { name: 'experiments', value: 'new-pricing-page', Path: '/home' },
//   { name: 'experiments', value: 'winter-launch', Path: '/home' },
// ]
response.cookies.getAll("experiments");
// Or omit the name entirely to get every cookie on the response
response.cookies.getAll();

Checking existence and deleting

response.cookies.has("experiments"); // true | false
response.cookies.delete("experiments"); // returns true if something was deleted

delete() is worth calling out because "deleting" a cookie over HTTP isn't really deletion in the way you'd delete a row from a database — there's no way to reach into the user's browser and erase it. What actually happens is Next.js sets a new Set-Cookie header for that name with an already-expired expires date (or a maxAge of zero), which tells the browser to drop it on its next pass. This is transparent to you as the developer — cookies.delete() handles the expiry mechanics — but it's worth understanding, because it means "deleting" a cookie is still, mechanically, setting a Set-Cookie header. If you've already sent response headers by some other path (unlikely with NextResponse, but possible if you're mixing raw Response construction with NextResponse cookie helpers), the delete won't take effect.

NextResponse.json()

A shorthand constructor for JSON API responses, functionally identical to Response.json() from the standard Web API, plus NextResponse's cookie/header conveniences layered on top:

// app/api/route.ts
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}

This sets the Content-Type: application/json header for you and serializes the object with JSON.stringify internally. The main mistake to avoid here: don't manually set Content-Type again after calling NextResponse.json() — it's already correct, and overriding it (say, in a proxy step that blanket-copies headers) can produce a response where the header says one thing and the body is another, which some HTTP clients will refuse to parse.

NextResponse.redirect()

Produces a response that tells the browser to navigate to a different URL. Because Next.js routing operates on full URL objects rather than bare path strings, you almost always construct the target with new URL(...) first:

import { NextResponse } from "next/server";

return NextResponse.redirect(new URL("/new", request.url));

The request.url argument is what makes this resolve correctly regardless of environment — it means /new gets resolved against whatever host and protocol the request actually arrived on, so this same code works identically on localhost:3000 in development and on your real domain in production, without you hardcoding either.

Because you're working with a real URL object before the redirect happens, you can mutate it — a pattern that comes up constantly for "redirect to login, but remember where the user was trying to go":

import { NextResponse } from "next/server";

// Given an incoming request...
const loginUrl = new URL("/login", request.url);
// Add ?from=/incoming-url to the /login URL
loginUrl.searchParams.set("from", request.nextUrl.pathname);
// And redirect to the new URL
return NextResponse.redirect(loginUrl);

Note the use of request.nextUrl.pathname here rather than something like parsing request.url yourself — nextUrl is a Next.js-specific convenience on the request object that gives you an already-parsed URL, which is worth knowing about even though this article is scoped to NextResponse rather than NextRequest.

A gotcha worth knowing: NextResponse.redirect() defaults to a 307 (Temporary Redirect) status code, which preserves the original HTTP method on the follow-up request. If a client POSTs to a route that redirects, the browser will POST again to the new location rather than downgrading to GET, which is correct per spec but occasionally surprises people expecting the older, sloppier 302 behavior. If you specifically need a permanent redirect that search engines should treat as a URL change, pass an explicit status: NextResponse.redirect(url, 308) (permanent, method-preserving) rather than assuming redirect() covers that case by default.

NextResponse.rewrite()

Rewrites are one of the more conceptually confusing parts of Next.js routing precisely because they're invisible to the end user by design. A rewrite serves content from a different internal path while the browser's address bar keeps showing the original URL:

import { NextResponse } from "next/server";

// Incoming request: /about, browser shows /about
// Rewritten request: /proxy, browser shows /about
return NextResponse.rewrite(new URL("/proxy", request.url));

This is the mechanism behind patterns like A/B testing (serve a different page variant from the same visible URL), internationalized routing without locale prefixes in the URL, and multi-tenant apps that route based on subdomain to different internal route trees while keeping a clean public URL. The critical mental model: a redirect is a message to the browser ("go somewhere else"); a rewrite is an instruction to the server ("serve different content, but don't tell the browser"). Confusing the two is a common source of bugs where developers expect the URL bar to update after what they thought was a redirect, and it doesn't, because they used rewrite() instead of redirect() — or the opposite, where an intended-to-be-invisible variant swap breaks bookmarking because a redirect() was used and the URL changed when it shouldn't have.

NextResponse.next()

This one only makes sense in the context of a proxy file (Next.js's request-interception layer, which replaced the old Middleware convention). next() tells Next.js "I'm done inspecting this request, continue routing it as normal" — it's the escape hatch that lets a proxy run some logic (auth checks, logging, header injection) without actually taking over the response itself.

import { NextResponse } from "next/server";

return NextResponse.next();

Forwarding modified headers upstream

The more advanced form lets you attach request headers that continue on to whatever handles the request next — a Server Component, a Route Handler, a Server Action:

import { NextResponse } from "next/server";

// 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,
  },
});

This is genuinely useful — a common pattern is decoding a session cookie in your proxy once, then forwarding the decoded user ID as a request header so every downstream Server Component doesn't have to re-verify the session token. But the Next.js docs are explicit about a real risk here: these forwarded headers are visible to any downstream consumer, including calls out to external services if your Server Components or Route Handlers proxy requests elsewhere. If you're forwarding anything derived from a secret (a decrypted user ID is usually fine; a raw unhashed token is not), think about whether that data should be crossing that boundary at all.

The header-to-the-client footgun

There's a second, easily confused form — NextResponse.next({ headers }), without the nested request key — that sends headers from the proxy to the client rather than upstream to your own server code. The docs flag this explicitly as something to avoid, and the reasoning is concrete rather than theoretical: if your proxy sets something like Content-Type this way, you can silently override the Content-Type Server Actions or streaming responses depend on internally, which manifests as failed form submissions or broken streaming with an error message that gives you almost no clue the proxy was the cause. If you've ever debugged a Server Action that inexplicably stopped working after adding an unrelated proxy header, this is worth checking first.

import { type NextRequest, NextResponse } from "next/server";

async function proxy(request: NextRequest) {
  const headers = await injectAuth(request.headers);
  // DO NOT forward headers like this
  return NextResponse.next({ headers });
}

Don't blanket-forward request headers

A related mistake, common enough that the docs call it out directly: copying all incoming request headers upstream without filtering. It's tempting to write new Headers(request.headers) and forward the whole thing verbatim, but incoming headers can include authorization, cookie, or custom x-* headers set by infrastructure in front of your app (a CDN, a load balancer) that were never meant to reach your application code, let alone anything downstream of it. The safer pattern is an allow-list rather than a deny-list — decide explicitly which headers are safe to forward, rather than trying to enumerate every header you don't want to leak:

import { type NextRequest, NextResponse } from "next/server";

function proxy(request: NextRequest) {
  const incoming = new Headers(request.headers);
  const forwarded = new Headers();

  for (const [name, value] of incoming) {
    const headerName = name.toLowerCase();
    if (
      !headerName.startsWith("x-") &&
      headerName !== "authorization" &&
      headerName !== "cookie"
    ) {
      forwarded.set(name, value);
    }
  }

  return NextResponse.next({
    request: {
      headers: forwarded,
    },
  });
}

An allow-list is more defensive because it fails safe: if someone adds a new sensitive header upstream of your app tomorrow and forgets to update your deny-list, a deny-list silently leaks it, while an allow-list simply drops it until you explicitly add it.

A Quick Comparison of the Four Methods

MethodWhat it doesBrowser URL barCommon use case
redirect()Tells the client to navigate elsewhereChangesAuth gates, moved content, canonical URL enforcement
rewrite()Serves different content internallyStays the sameA/B tests, i18n without URL prefixes, multi-tenant routing
next()Passes the request through, optionally with modified headersStays the sameAuth checks, logging, injecting derived data for downstream code
json()Shorthand for a JSON API responseN/A (API response)Route Handlers returning structured data

Common Mistakes, Collected

  • Reading response.cookies.get() expecting incoming request cookies. You want request.cookies.get() for that — the response's cookie jar only has what you've explicitly set on it.
  • Using redirect() when you meant rewrite() (or vice versa). If the URL bar changing (or not changing) surprises you in testing, you've reached for the wrong one.
  • Assuming a 307 redirect behaves like an old-style 302. Method-preserving redirects mean a POST stays a POST — this is spec-correct but can break code written assuming redirects always downgrade to GET.
  • Blanket-forwarding all request headers upstream. Build an allow-list instead of trusting every header that happened to arrive.
  • Setting response headers via the flat NextResponse.next({ headers }) form. Use the nested { request: { headers } } form unless you specifically intend to hand something to the client, and even then, double-check you're not clobbering a framework-managed header like Content-Type.

Key Takeaways

NextResponse is a thin, purpose-built layer over the standard Web Response API — it doesn't replace your understanding of HTTP responses, it just removes the boilerplate around the handful of operations Next.js routing needs constantly: setting and reading cookies safely, redirecting with mutable URLs, rewriting requests invisibly, and passing requests through with optionally modified headers. The API surface is small enough to hold in your head entirely, and once you separate "changes the URL" (redirect) from "doesn't" (rewrite, next), most of the confusion around when to reach for which method disappears. The header-forwarding methods are the one place genuine security judgment is required — treat any header you forward upstream or expose to the client as something you're actively choosing to share, not something that happened by default.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper