Topic 06 / 13
Route Handlers — Building APIs
APIs Inside Your App
A route.js file exports HTTP-method functions and becomes an endpoint — same filesystem routing as pages:
app/
└── api/
├── courses/
│ ├── route.js # /api/courses
│ └── [id]/
│ └── route.js # /api/courses/123// app/api/courses/route.js
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
export async function GET(request) {
const { searchParams } = new URL(request.url);
const track = searchParams.get("track");
const courses = await db.course.findMany(
track ? { where: { track } } : undefined
);
return NextResponse.json(courses);
}
export async function POST(request) {
const body = await request.json();
if (!body.title) {
return NextResponse.json(
{ error: "title is required" },
{ status: 400 }
);
}
const course = await db.course.create({ data: body });
return NextResponse.json(course, { status: 201 });
}Handlers use the web-standard Request/Response objects — the same API as fetch, service workers, and other modern runtimes.
Dynamic Params, Headers & Cookies
// app/api/courses/[id]/route.js
export async function GET(request, { params }) {
const { id } = await params;
const course = await db.course.findUnique({ where: { id: Number(id) } });
if (!course) {
return NextResponse.json({ error: "not found" }, { status: 404 });
}
return NextResponse.json(course);
}
export async function DELETE(request, { params }) {
const { id } = await params;
await db.course.delete({ where: { id: Number(id) } });
return new Response(null, { status: 204 });
}import { cookies, headers } from "next/headers";
export async function GET() {
const cookieStore = await cookies();
const session = cookieStore.get("session")?.value;
if (!session) {
return NextResponse.json({ error: "unauthorised" }, { status: 401 });
}
...
}Validation with Zod
Never trust a request body:
import { z } from "zod";
const CourseSchema = z.object({
title: z.string().min(3),
price: z.number().int().nonnegative(),
});
export async function POST(request) {
const parsed = CourseSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ errors: parsed.error.flatten().fieldErrors },
{ status: 422 }
);
}
const course = await db.course.create({ data: parsed.data });
return NextResponse.json(course, { status: 201 });
}When Do You Actually Need These?
Inside one Next app, often you don’t: server components read data directly, and mutations are usually better as server actions (next topic). Route handlers earn their place for:
- External consumers — mobile apps, third parties, another frontend.
- Webhooks — Stripe, GitHub, YouTube push events to a URL.
- Client-side libraries that need an endpoint — TanStack Query polling, file upload targets.
- Non-JSON responses — RSS feeds, sitemaps, generated images.