Skip to main content

Route Handlers

A Route Handler is a route.ts file in app/ that exports functions named after HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). Each takes a Web Request (or NextRequest) and returns a Response. It is the App Router replacement for API Routes (pages/api) and is used for endpoints called by something other than your own UI: webhooks, mobile apps, OAuth callbacks, files.

Applies to: Next.js 16.4.

Details​

  • The URL comes from the folder: app/api/users/[id]/route.ts → /api/users/1. The api/ prefix is a convention, not a requirement.
  • The second argument holds params; as in pages, it is a promise.
  • Query string: request.nextUrl.searchParams. Body: await request.json() or await request.formData().
  • cookies() and headers() from next/headers work here, and cookies can be set.
  • Responses: Response.json(data, { status }), NextResponse.json(), NextResponse.redirect(new URL(...)), or a stream.
  • Caching: GET handlers are not cached by default since Next.js 15. In the previous model, export const dynamic = 'force-static' caches one. With Cache Components, GET handlers follow the same model as pages: prerendered only when they read no uncached or request data, and 'use cache' goes into a helper function.

Route Handler vs Server Action​

Route HandlerServer Action
Called byAny HTTP clientYour React components (forms, event handlers)
HTTP methodsAnyPOST only
URLExplicit, from the file pathGenerated ID, sent to the page that uses it
ReturnsAny Response: JSON, files, streamsA serializable value plus an updated UI
Typical useWebhooks, a public API, OAuth callbacksMutations from forms and buttons

Both are public endpoints: check the session and validate the input in each one.

Example​

app/api/users/[id]/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { z } from 'zod';
import { getSession } from '@/lib/session'; // returns null, does not redirect
import { getUser, updateUser } from '@/lib/users';

type Context = { params: Promise<{ id: string }> };

const UpdateSchema = z.object({ name: z.string().min(1).max(100) });

// Public on purpose: returns only public fields
export async function GET(_request: NextRequest, { params }: Context) {
const { id } = await params;
const user = await getUser(id);
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json({ id: user.id, name: user.name });
}

export async function PATCH(request: NextRequest, { params }: Context) {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
if (session.userId !== id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const parsed = UpdateSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json({ error: 'Invalid body' }, { status: 400 });
}
return NextResponse.json(await updateUser(id, parsed.data));
}

Follow-up Questions​

  • How do you type the second argument? The global RouteContext<'/api/users/[id]'> helper, generated by next dev, next build or next typegen.
  • How do you stream a response? Return new Response(readableStream), for example for LLM output or a large file.
  • What about OPTIONS and CORS? Export OPTIONS that returns the Access-Control-Allow-* headers for the preflight, and set them on the real responses; without an OPTIONS export Next.js answers only with Allow.

Common Mistakes​

  • Calling your own Route Handler with fetch from a Server Component. That is an extra HTTP round trip on the same server; call the data function directly.
  • Using a Route Handler for a form in your own app, where a Server Action is simpler and works without JavaScript.

See Also​