Skip to content
C.W.K.
Stream
Lesson 01 of 09 · published

Route Handlers

~20 min · route handler, API, REST

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

When you actually need an API route

Server Actions cover internal mutations. Server Components cover internal data reads. Route Handlers cover the rest: webhooks, third-party integrations, and public APIs — anything that exposes a stable URL the outside world can call.

The shape

A route.ts in any folder under app/ exports HTTP method handlers (GET, POST, PUT, DELETE, etc.). Each receives a NextRequest and returns a NextResponse.

Hard rule

A folder with route.ts can't also have page.tsx. They serve different worlds (API vs UI). Pick one per folder.

Choose a Route Handler when the contract itself is an HTTP endpoint: a webhook, public API, file response, health check, or integration another system must call by URL. For an internal form mutation, prefer a Server Action; for an internal read, fetch in the Server Component that owns the view.

A familiar REST shape is not a reason to put an API between your own server-rendered page and its database. That extra hop adds serialization, authentication plumbing, and another cache boundary. The handler earns its place only when the URL is part of the product contract. Call the handler without the application UI. Verify method handling, input validation, authentication, status codes, response headers, and the missing-resource case. For a webhook, replay the same signed event and prove the write is idempotent.

Code

GET + POST handlers·ts
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';

export async function GET(request: NextRequest) {
  const params = request.nextUrl.searchParams;
  const page = Number(params.get('page') ?? 1);
  const posts = await db.post.findMany({ skip: (page - 1) * 10, take: 10 });
  return NextResponse.json(posts);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const post = await db.post.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}
Dynamic route handler with params·ts
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const post = await db.post.findUnique({ where: { id } });
  if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  return NextResponse.json(post);
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  await db.post.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}

External links

Exercise

Build a webhook endpoint at app/api/stripe/route.ts that verifies a Stripe signature header and writes to your database. Test it with the Stripe CLI's stripe listen.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.