HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•100 Minutes

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.

GET Route Handler
import { NextResponse } from 'next/server';

export async function GET() {
  const users = await db.user.findMany();

  return NextResponse.json({ users });
}
💡 The GET handler retrieves data and returns a JSON response.
Key Takeaways
  • 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.

Validated POST Handler
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 });
}
💡 The handler validates input, authenticates the request, performs the mutation, and returns a meaningful HTTP status.
Key Takeaways
  • 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.

Consistent API Error
return NextResponse.json(
  {
    error: {
      code: 'RESOURCE_NOT_FOUND',
      message: 'Product was not found'
    }
  },
  { status: 404 }
);
💡 A stable error structure gives API consumers a predictable contract.
Key Takeaways
  • 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.

Query Parameters
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 });
}
💡 The handler constrains pagination parameters instead of trusting arbitrary values.
Key Takeaways
  • 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.

Design for Retries

Webhook providers can retry events. Store event identifiers and make processing idempotent to prevent duplicate side effects.

Webhook Skeleton
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');
}
💡 Signature verification should happen before trusting the webhook payload.
Key Takeaways
  • Verify webhook authenticity.
  • Expect duplicate deliveries.
  • Make webhook processing idempotent.