The basics of route.ts cover JSON in and JSON out. Production endpoints also set cookies, answer preflight requests from other origins, verify webhook signatures and finish work after the response is sent. After this lesson you will be able to handle all of these with NextRequest, NextResponse and the helpers in next/server.
| Need | API |
|---|---|
| Query string | request.nextUrl.searchParams.get("q") |
| JSON, text or form body | await request.json(), .text(), .formData() |
| Read cookies | request.cookies.get("token")?.value or (await cookies()).get("token") |
| Read headers | request.headers.get("authorization") or await headers() |
| JSON response | NextResponse.json(data, { status: 201 }) |
| Dynamic params | Second argument: { params }: { params: Promise<{ id: string }> } |
Both classes extend the Web Request and Response, so Fetch API code works unchanged.
Reading a cookie and setting one on the way out:
// app/api/session/route.ts
import { NextResponse, type NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const { userId } = await request.json();
const token = await createSessionToken(userId);
const response = NextResponse.json({ ok: true });
response.cookies.set("session", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 7,
});
return response;
}To log out, read request.cookies.get("session")?.value, revoke it, and call response.cookies.delete("session"). The cookies() function from next/headers also works in handlers. Reading cookies or headers makes a GET handler dynamic.
A mobile app or a different domain calling your API needs CORS headers, including an OPTIONS handler for the browser's preflight request:
// app/api/public/quotes/route.ts
import { NextResponse } from "next/server";
const cors = {
"Access-Control-Allow-Origin": "https://app.example.com",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: cors });
}
export async function GET() {
const quotes = await getQuotes();
return NextResponse.json(quotes, { headers: cors });
}For headers shared by many routes, the headers() option in next.config.ts or the proxy file (next lesson) is less repetitive.
Payment providers and Git hosts POST signed events to your server. The sample at the top of this lesson shows the three rules: read the raw body with request.text() because the signature covers the exact bytes, compute your own HMAC with the shared secret, and compare with timingSafeEqual to avoid timing attacks. Only then parse the JSON. Respond quickly with a 2xx; providers retry on failures.
Logging, analytics and notifications should not delay the reply. The after function schedules work to run once the response has been sent:
import { after, NextResponse } from "next/server";
export async function POST(request: Request) {
const order = await createOrder(await request.json());
after(async () => {
await sendConfirmationEmail(order); // runs after the client has its response
});
return NextResponse.json(order, { status: 201 });
}Handlers can also stream by passing a ReadableStream to new Response(), which is how chat endpoints deliver tokens incrementally.
request.json() before verifying the signature; re-serialised JSON will not match.OPTIONS handler, so browsers fail the preflight even though GET sends the right headers.Why must a webhook handler read the body with `request.text()` instead of `request.json()`?
NextRequest and NextResponse extend the Web Fetch API with cookies, nextUrl and JSON helpers.httpOnly, secure and sameSite options.OPTIONS preflight handler.timingSafeEqual before parsing.after() for work that should not delay the response.Next lesson: Proxy (Middleware): Redirects, Rewrites and Request Interception ā run code before a request reaches any route.