Topic 06 / 13

Route Handlers — Building APIs

~9 min read  //  Next.js Series  //  Coding India

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.