R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 31 of 42Article17 min

API Routes & Route Handlers

In the App Router, API endpoints are Route Handlers. Create a folder under app/api and export functions named GET, POST, PUT, PATCH, DELETE from route.ts.

app/api/.../route.ts

In the App Router, API endpoints are Route Handlers. Create a folder under app/api and export functions named GET, POST, PUT, PATCH, DELETE from route.ts.

Each file maps to a URL path. app/api/hello/route.ts → /api/hello.

Real-life example: A Route Handler is a small service window at the back of the shop — browsers and mobile apps knock on /api/... and get JSON, not a full HTML page.

  • Export named HTTP methods from route.ts
  • Use Request and Response (Web standard APIs)
  • Prefer Server Actions for form mutations from your own UI; use Route Handlers for public APIs and webhooks
Tip: Keep Route Handlers thin — validate input, call a lib function, return JSON. Fat business logic belongs in shared modules.

GET and POST examples

GET reads data. POST creates or triggers work. Always return the right status codes: 200/201 success, 400 bad input, 401 unauthorized, 404 missing, 500 server error.

Real-life example: GET is asking the library for a book list. POST is submitting a new book donation form.

app/api/courses/route.ts — GET + POST
import { NextRequest, NextResponse } from "next/server";

const courses = [
  { id: "1", title: "Next.js Zero to Full-Stack" },
  { id: "2", title: "React Hooks Deep Dive" },
];

export async function GET() {
  return NextResponse.json(courses);
}

export async function POST(req: NextRequest) {
  const body = await req.json();
  const title = String(body.title ?? "").trim();

  if (!title) {
    return NextResponse.json(
      { error: "title is required" },
      { status: 400 }
    );
  }

  const created = { id: String(courses.length + 1), title };
  courses.push(created);

  return NextResponse.json(created, { status: 201 });
}
Calling the API from a Client Component
"use client";

import { useState } from "react";

export function AddCourseForm() {
  const [title, setTitle] = useState("");
  const [message, setMessage] = useState("");

  async function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    const res = await fetch("/api/courses", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title }),
    });
    const data = await res.json();
    setMessage(res.ok ? `Saved: ${data.title}` : data.error);
  }

  return (
    <form onSubmit={onSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Course title"
      />
      <button type="submit">Add</button>
      {message && <p>{message}</p>}
    </form>
  );
}

Building and managing API routes

Group related endpoints under folders. Share helpers for auth checks and validation. Document expected JSON shapes for teammates.

For webhooks (Stripe, GitHub), verify signatures before trusting the body.

Real-life example: Managing API routes is organizing a filing cabinet — one drawer per topic (users, courses, webhooks), labeled clearly so nobody digs in the wrong place.

  • app/api/users/route.ts — collection
  • app/api/users/[id]/route.ts — one item
  • app/api/webhooks/stripe/route.ts — external events
Shared JSON error helper
import { NextResponse } from "next/server";

export function jsonError(message: string, status = 400) {
  return NextResponse.json({ error: message }, { status });
}

// usage: return jsonError("Unauthorized", 401);