The first argument is a NextRequest, a subclass of the Web Request with two useful additions: request.nextUrl, a parsed URL object, and request.cookies, a typed cookie jar. Everything else — headers, json(), text(), formData(), arrayBuffer() — is the standard API you already use with fetch.
A second argument carries params, and since Next.js 15 10,514 it is a promise you must await, the change that lets the framework begin rendering before segment values resolve. It applies to pages and layouts too.
import { type NextRequest } from 'next/server'
import { products } from '@/lib/products'
type Ctx = { params: Promise<{ id: string }> }
export async function GET(request: NextRequest, { params }: Ctx) {
const { id } = await params
const product = products.find((p) => p.id === id)
if (!product) return Response.json({ error: `no product ${id}` }, { status: 404 })
return Response.json(product)
}A GET of /api/products/p2 returns {"id":"p2","name":"USB-C hub","price":45,"tag":"adapter"}, while /api/products/p99 comes back HTTP/1.1 404 Not Found with the error body.
There is a typed alternative to that hand-written Ctx. Declaring the second argument as ctx: RouteContext<'/api/products/[id]'> uses a global helper Next.js generates from your real folder tree during next dev, next build or next typegen. It needs no import and derives the shape of ctx.params from the route literal, so a renamed folder becomes a type error rather than an undefined at runtime.
request.json() throws on malformed input, so wrap it rather than letting a bad client produce a 500. For webhooks read request.text() instead: signature verification at Stripe 238 or GitHub 29 hashes the exact bytes sent, and re-serializing a parsed object will not reproduce them. Unlike Pages Router API routes there is no bodyParser to disable — nothing touches the body until you ask.
request.formData() handles both application/x-www-form-urlencoded and multipart/form-data, so uploads need no extra dependency; a file arrives as a Web File with .name, .size and .stream(). Every value is a string or a File, never a number, so validate before you trust it — Validating Input with Zod uses Zod 44,027 for exactly that, and the same schema works unchanged here.