A route handler receives everything about the request through its Request argument. The body, the query string, and the headers are all readable with the standard Web APIs, plus a few Next.js helpers.
Which read method you use depends on the content the client sends. Here is a POST handler reading a JSON body.
// app/api/posts/route.ts
export async function POST(request: Request) {
const body = await request.json()
return Response.json({ received: body })
}The handler parses the JSON body once and echoes it back. Request bodies are streams, so request.json() consumes the body and you can only call one read method per request.
Read JSON, text, and FormData
Choose the read method by content type. Use request.json() for a JSON payload, request.text() for raw text or webhooks, and request.formData() for form submissions or file uploads. The client signals the format through the Content-Type header, but you decide which read method to call based on the endpoint contract.
// app/api/contact/route.ts
export async function POST(request: Request) {
const form = await request.formData()
const name = form.get('name')
const email = form.get('email')
if (typeof name !== 'string' || typeof email !== 'string') {
return Response.json({ error: 'Invalid form fields' }, { status: 400 })
}
return Response.json({ name, email })
}This reads a submitted form and returns the two text fields. A FormData entry can be a string, a File, or null when the key is missing, so check its type before using it and convert numeric strings explicitly.
Read raw text for webhooks
Webhook providers usually send JSON or text that you should read exactly once with request.text(). Reading the raw string first is also the pattern for verifying a signature, because verification needs the exact bytes before you parse them.
After you have the string, parse it with JSON.parse when the payload is JSON. Keep the raw value around if your provider requires a signature check over the original body. A signature check typically hashes the raw body with a secret, so read the text before any parsing.
Read query parameters
Query parameters live in the URL, not the body. Access them through NextRequest and its nextUrl.searchParams property, which returns a URLSearchParams object.
// app/api/search/route.ts
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const q = request.nextUrl.searchParams.get('q')
return Response.json({ q })
}A request to /api/search?q=nextjs returns q with the value nextjs. Use get for a single value and getAll for a repeated key.
URLSearchParams also exposes has, entries, and toString, so you can check presence or rebuild the query string. Treat every query value as a string until you convert or validate it.
Read request headers
Use headers() from next/headers to read request headers. It returns a read-only headers list, and it is async in the App Router, so await it first.
// app/api/profile/route.ts
import { headers } from 'next/headers'
export async function GET() {
const headersList = await headers()
const token = headersList.get('authorization')
return Response.json({ hasToken: Boolean(token) })
}This checks whether the request carried an authorization header. You can also read request.headers directly from the request object for a plain Headers view, while the next/headers version is the App Router convention and is read-only.
To send headers back, return a new Response with a headers option. See returning JSON, files, streams, and redirects for response headers.
For the full set of methods that receive these values, see handling GET, POST, PUT, PATCH, and DELETE, and for the request helpers themselves, see NextRequest and NextResponse.
Validate what you read
The request body and query params are untrusted input. Validate them on the server before you store or return them, because a route handler is reachable over HTTP by any client. Reject early with a 400 response when validation fails, so the client learns what to fix.
A schema such as Zod turns FormData strings and JSON objects into typed, checked values. The same server-side validation you apply to Server Actions keeps malformed input away from your data layer.
Common mistakes
Reading the body twice is the most common mistake. request.json() consumes the stream, so a second request.json() or request.text() throws. Read once and store the result.
Forgetting to await headers() is the other one. In Next.js 16, headers() returns a Promise, so calling .get() before resolving it is invalid and TypeScript reports the mistake.
Assuming a query param is always present is a third trap. A missing key returns null from get, so handle that case instead of passing null onward.
Rune AI
Key Insights
- Read JSON with request.json() and FormData with request.formData().
- A request body can only be read once, so store the result.
- Query params come from request.nextUrl.searchParams.
- Use await headers() for read-only header access.
- Set response headers by returning a new Response instead.
Frequently Asked Questions
Can I read the request body more than once?
How do I read query parameters in a route handler?
How do I set headers in a route handler?
Conclusion
Route handlers read input through the Web Request API. Use request.json(), request.text(), or request.formData() for the body, request.nextUrl.searchParams for query values, and await headers() for request headers. Each body read consumes the stream once, and FormData entries still need type checks and validation.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.