Route Handlers and Full-Stack API Development
Build backend endpoints inside Next.js using Route Handlers, HTTP methods, validation, authentication, errors, pagination, and API design principles.
Route Handlers
Route Handlers provide HTTP endpoints inside the App Router. A route.ts file can define handlers for methods such as GET, POST, PUT, PATCH, and DELETE.
Route Handlers are appropriate when an API must be consumed by external clients, browser code, webhooks, mobile applications, or third-party integrations.
If only a Server Component needs server-side data, directly calling the data layer is often simpler than creating an internal HTTP request.
import { NextResponse } from 'next/server';
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json({ users });
}- Route Handlers implement HTTP endpoints.
- Use them for genuine API boundaries.
- Avoid unnecessary internal HTTP calls from Server Components.
POST Requests and Request Bodies
POST handlers typically parse JSON or form data and then validate it before performing a mutation.
Never directly trust the request body. Clients can send malformed fields, unexpected types, oversized values, or malicious input.
A good handler separates parsing, validation, authorization, business logic, and response formatting.
import { NextResponse } from 'next/server';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2)
});
export async function POST(request: Request) {
const body = await request.json();
const result = schema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid request' },
{ status: 400 }
);
}
const user = await requireUser();
const created = await createUser(result.data, user.id);
return NextResponse.json(created, { status: 201 });
}- Parse and validate request bodies.
- Authenticate and authorize before protected mutations.
- Return appropriate HTTP status codes.
API Error Handling
APIs should provide predictable error structures. Avoid returning raw database exceptions or stack traces to clients.
Differentiate client errors such as invalid input from authentication failures, authorization failures, missing resources, conflicts, and unexpected server failures.
A consistent error format makes frontend and external integrations easier to maintain.
return NextResponse.json(
{
error: {
code: 'RESOURCE_NOT_FOUND',
message: 'Product was not found'
}
},
{ status: 404 }
);- Do not leak internal exceptions.
- Use meaningful status codes.
- Define a consistent error response contract.
Pagination, Filtering and Sorting
Large APIs should not return unlimited records. Pagination protects database performance, memory consumption, and response size.
Offset pagination is straightforward and works well for many administrative interfaces. Cursor pagination can be more stable for rapidly changing datasets and large collections.
Filtering and sorting parameters should be validated and constrained. Never directly concatenate arbitrary user input into database queries.
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = Math.max(1, Number(searchParams.get('page') ?? 1));
const limit = Math.min(100, Math.max(1, Number(searchParams.get('limit') ?? 20)));
const users = await getUsers({ page, limit });
return Response.json({ users, page, limit });
}- Always bound pagination parameters.
- Choose cursor or offset pagination based on workload.
- Validate sorting and filtering inputs.
Building Webhook Endpoints
Webhook endpoints receive requests from external systems such as payment providers, GitHub, email platforms, or deployment systems.
Security is especially important because webhooks are public endpoints. Providers commonly sign requests using a secret so your server can verify authenticity.
Webhook handlers should be idempotent where possible because providers may retry the same event.
Webhook providers can retry events. Store event identifiers and make processing idempotent to prevent duplicate side effects.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get('x-webhook-signature');
verifySignature(rawBody, signature);
const event = JSON.parse(rawBody);
await processWebhookEvent(event);
return new Response('OK');
}- Verify webhook authenticity.
- Expect duplicate deliveries.
- Make webhook processing idempotent.