Reading the Request Body, Query Params, and Headers

How to read JSON, text, and FormData bodies, parse query parameters, and access headers inside a Next.js route handler.

6 min read

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.

typescripttypescript
// 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.

typescripttypescript
// 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.

typescripttypescript
// 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.

typescripttypescript
// 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Can I read the request body more than once?

No. A Request body is a one-time stream, so request.json() or request.text() consumes it. Call one of them once and store the result. Calling a second read method throws because the body is already used.

How do I read query parameters in a route handler?

Use request.nextUrl.searchParams, which is a URLSearchParams object. Call get to read one value or getAll for repeated keys. Import the NextRequest type from next/server for typed access.

How do I set headers in a route handler?

The headers from next/headers are read-only. To send headers, return a new Response or NextResponse with a headers option.

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.