skip to content

In a Next.js App Router route handler, what does the NextRequest argument add on top of the standard Web Request, and how do you read a query-string parameter and a cookie from it?

level: middleimportance: should knowfreq 55%

answer

  1. a subclass, not a replacement
  2. the URL comes pre-parsed
  3. cookies come back as objects
  4. nextUrl.searchParams.get
  5. body stream reads only once

basics

~10 s

NextRequest extends the Web Request, so request.json(), request.headers and request.formData() still work. It adds request.nextUrl, an already-parsed URL whose searchParams give query values, and request.cookies with get, getAll and has for reading cookies.

solid answer

~40 s

`NextRequest` is a subclass of the standard Web `Request`, so everything on the platform object is still there: `await request.json()`, `await request.formData()`, `await request.text()`, `request.headers`, `request.method`, `request.signal`. Next adds two conveniences. `request.nextUrl` is the incoming URL already parsed, so `request.nextUrl.searchParams.get('page')` reads a query parameter without you constructing `new URL(request.url)` yourself, and it also exposes `pathname`. `request.cookies` is a cookie store with `get`, `getAll`, `has`, `set` and `delete`; `request.cookies.get('session')` returns an object with `name` and `value`, not a bare string — a classic slip. Inside a route handler you can equally call `cookies()` and `headers()` from `next/headers`, which return Promises in Next.js 15 and 16 and must be awaited. Whichever you use, touching request-time input is what marks the handler as per-request rather than cacheable.

code

typescript · 14 lines
typescript
// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const q = request.nextUrl.searchParams.get('q') ?? ''
  const page = Number(request.nextUrl.searchParams.get('page') ?? '1')
  const session = request.cookies.get('session')

  if (!session) {
    return NextResponse.json({ error: 'unauthenticated' }, { status: 401 })
  }

  return NextResponse.json({ q, page, token: session.value })
}

go deeper

for a junior

Know that the handler's first argument is the incoming request, that a JSON body is read with await request.json(), and that query values come from a parsed URL rather than from string slicing.

for a middle

Explain that NextRequest extends the Web Request and adds only nextUrl and cookies, demonstrate reading a query parameter and a cookie, and mention that the body stream can be consumed only once.

for a senior

Show care with untrusted input: validate query values and bodies against a schema before use, coerce types deliberately, and recognise that reading request data is exactly what keeps the handler executing per request.

for a principal

Set the house pattern — a shared parsing and validation layer, one error envelope across every endpoint, and explicit rules about which headers and cookies handlers may depend on so behaviour stays predictable behind proxies.

## Start from the platform object Route handlers were designed around the Web platform rather than around a Next-specific request abstraction. The object your handler receives is a `Request` — the same interface `fetch` produces on the client — and everything you know about it applies: ```ts export async function POST(request: Request) { const contentType = request.headers.get('content-type') const body = await request.json() return Response.json({ contentType, body }) } ``` That matters for portability and for testing: you can construct a `new Request('https://example.com/api/x', { method: 'POST', body })` in a unit test and call the exported handler directly, with no Next server running. `NextRequest` is a subclass of that object. It never removes anything; it adds two properties that are annoying to derive by hand. ## nextUrl `request.url` is a string. To read a query parameter from it you would write `new URL(request.url).searchParams.get('q')`, re-parsing on every access. `request.nextUrl` is that parse done once, exposed as a URL-like object: ```ts export async function GET(request: NextRequest) { const q = request.nextUrl.searchParams.get('q') ?? '' const tags = request.nextUrl.searchParams.getAll('tag') const path = request.nextUrl.pathname return Response.json({ q, tags, path }) } ``` `searchParams` is a standard `URLSearchParams`, so `get` returns the first value or `null`, `getAll` returns every occurrence of a repeated key, and `has` tests presence. Two habits worth having: coerce numerics explicitly (`Number(sp.get('page') ?? '1')`, since everything arrives as a string), and validate rather than trust — query strings are attacker-controlled input like any other. ## cookies `request.cookies` is a cookie store, not a plain object. The methods are `get`, `getAll`, `has`, `set` and `delete`. The single most common mistake is assuming `get` hands back the value: ```ts const raw = request.cookies.get('session') // { name: 'session', value: '...' } | undefined const value = request.cookies.get('session')?.value ``` Because `get` returns an object, a truthiness check on it tells you the cookie exists but says nothing about its contents, and passing it straight into a string comparison silently fails. ## The next/headers alternative Inside a route handler you can also import `cookies()`, `headers()` and `draftMode()` from `next/headers`. These read the same incoming request through Next's request context rather than through the argument, which is what lets a helper function deep in your call stack read a cookie without threading the request object down to it: ```ts import { cookies } from 'next/headers' export async function GET() { const store = await cookies() const session = store.get('session')?.value return Response.json({ signedIn: Boolean(session) }) } ``` In Next.js 15 and 16 these functions return Promises and must be awaited; earlier versions returned the store synchronously, which is why older snippets omit the `await`. `cookies().set()` and `.delete()` are usable from a route handler, unlike from a Server Component where the response headers are already committed. ## The body is a one-shot stream `request.json()`, `request.text()`, `request.formData()` and `request.arrayBuffer()` all consume the same underlying stream. Calling two of them on one request throws, because the body has already been read. If you genuinely need both the raw text and the parsed object — verifying a webhook signature over the exact bytes, say — read the text once and parse it yourself, or take `request.clone()` before consuming. ## What you return Any Web `Response`. `Response.json(value, { status })` covers most cases; `NextResponse.json(value, init)` is the Next subclass and behaves the same for a straightforward JSON reply. Set a status explicitly for anything other than 200 — a 400 returned with a body describing the validation failure but a 200 status is worse than useless to a caller. ## The caching consequence Reading request-time input is not free of meaning: a handler that consults `request.nextUrl`, `request.cookies`, `cookies()` or `headers()` cannot be evaluated once at build time, because there is no request to read. That is the mechanical reason such a handler stays per-request, and the reason a handler forced into static evaluation sees empty values from those APIs instead of real ones. ## Mistakes to avoid Treating `request.cookies.get(name)` as a string. Re-parsing `request.url` with a regex instead of using `searchParams`. Assuming a query parameter is a number. Reading the body twice. And assuming `cookies()` is still synchronous on a current Next.js major.

  • Is there ever a reason to use new URL(request.url) instead of request.nextUrl?
    Portability. `request.url` is standard on the Web `Request`, so a handler written against it moves to any fetch-based runtime unchanged and is trivial to unit-test with a hand-built `Request`. `nextUrl` is Next-specific but already parsed, so it avoids re-parsing on every read. Inside a Next codebase, `nextUrl` is the idiomatic choice.
  • How does request.cookies differ from cookies() imported from next/headers?
    `request.cookies` reads the cookies on the request object you were handed. `cookies()` reads the same incoming cookies through Next's request context, so a helper several calls deep can reach them without the request being threaded down. In a route handler `cookies()` can also set and delete; in Next.js 15 and 16 it returns a Promise and must be awaited.
  • What happens if a handler reads the request body twice?
    The second read throws. The body is a stream and is consumed by the first of `json()`, `text()`, `formData()` or `arrayBuffer()`. When you need the raw bytes and the parsed value — webhook signature verification is the usual case — read the text once and parse it yourself, or call `request.clone()` before consuming the original.

saying these in an interview costs you the question

  • Treats request.cookies.get() as returning the raw string
  • Says NextRequest replaces the Web Request API entirely
  • Reads the request body twice in the same handler
  • Parses the query string out of request.url with a regex
  • Assumes cookies() is synchronous on current Next.js

context