█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Web Development/React and Next.js/API Routes in Next.js
45 minIntermediate

API Routes in Next.js

After this lesson, you will be able to: Build REST API endpoints inside your Next.js app using the App Router's `route.ts` files, handle GET and POST requests with proper status codes, validate inputs with Zod, and call them from your client components.

API routes give your Next.js app a backend without spinning up a separate server. Create a `route.ts` file under `app/api/`, export functions named after HTTP methods, and you have endpoints, deployed to the same Vercel infrastructure as your pages, automatically. This lesson takes you from never having written an API route to building a small REST API for your own app: handle requests, validate input, return JSON with correct status codes.

Prerequisites:Server Components vs Client Components

What an API route is in Next.js

A `route.ts` file in the `app/api/` folder. Export functions named `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, those are the HTTP methods the endpoint handles. Each function receives the standard Web `Request` object and returns a `Response`. Folder name = URL segment, same as pages. `app/api/posts/route.ts` → `POST /api/posts`. `app/api/posts/[id]/route.ts` → `GET /api/posts/123`. The same file-based mental model as the App Router.

A GET route returning JSON

Simplest endpoint: query a list, return it as JSON. `NextResponse.json` sets the right Content-Type for you and is the standard helper.

python
// app/api/posts/route.ts
import { NextResponse } from "next/server";
// In real apps, this comes from a database (Supabase, Postgres via Prisma, etc.)
const posts = [
{ id: 1, title: "Hello Next.js" },
{ id: 2, title: "Why I love TypeScript" },
];
export async function GET() {
return NextResponse.json(posts);
}

A POST route with input validation

Real APIs validate input. Zod is the standard TS-first validator, declare the shape once, get parsing AND types from it. Reject bad input with a 400, otherwise return 201 with the created resource.

python
// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
const postSchema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(1),
});
let posts = [{ id: 1, title: "Hello", body: "..." }];
let nextId = 2;
export async function POST(request: Request) {
const body = await request.json().catch(() => null);
const result = postSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: "Invalid input", details: result.error.issues },
{ status: 400 },
);
}
const post = { id: nextId++, ...result.data };
posts.push(post);
return NextResponse.json(post, { status: 201 });
}

Dynamic API routes

Same `[param]/` syntax as pages. The `params` object is a Promise in Next 15+, so await it.

python
// app/api/posts/[id]/route.ts
import { NextResponse } from "next/server";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const post = posts.find((p) => p.id === Number(id));
if (!post) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(post);
}

Error handling and status codes

The wd-apis lesson covered HTTP status codes, apply them on the SERVER side now. Successful GET → 200. Successful POST that created a resource → 201. Bad input → 400. Missing auth → 401. Authenticated but forbidden → 403. Not found → 404. Server error → 500. Wrap risky operations in try/catch so a database failure returns a clean 500 instead of crashing the server.

Calling your API from a Client Component

Same `fetch` pattern as the wd-apis lesson. Use a relative URL since the API lives on the same origin.

python
"use client";
import { useState } from "react";
export function NewPostForm() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [status, setStatus] = useState<"idle" | "saving" | "error" | "saved">("idle");
async function submit(event: React.FormEvent) {
event.preventDefault();
setStatus("saving");
try {
const response = await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setStatus("saved");
setTitle("");
setBody("");
} catch {
setStatus("error");
}
}
return (
<form onSubmit={submit} className="space-y-3">
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" className="w-full border p-2" />
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Body" className="w-full border p-2" />
<button disabled={status === "saving"} className="rounded bg-slate-900 px-4 py-2 text-white">
{status === "saving" ? "Saving..." : "Create post"}
</button>
{status === "error" && <p className="text-red-600">Could not save.</p>}
</form>
);
}

Add a DELETE endpoint

Extend `app/api/posts/[id]/route.ts` to handle a DELETE request. Remove the matching post from the array and return 204 No Content on success, 404 if the post doesn't exist. Required patterns check for the `DELETE` export, the params await, and the correct status codes.

Loading exercise…

ℹ️ Common mistakes only experienced devs catch

Five API-route traps. (1) **Putting writes in GET**, GETs must be safe and idempotent. Every cache, prefetcher, and crawler will hit them. Writes go in POST/PUT/DELETE. (2) **Trusting the request body**, always validate with Zod (or similar). Without validation, bad input crashes your handler or corrupts your data. (3) **Forgetting `await request.json()`**, `request.json()` is a Promise. Treating the result like a sync object reads `undefined`. (4) **Returning sensitive errors to the client**, stack traces in 500 responses leak internal info. Return a generic message; log the details server-side. (5) **Skipping rate limiting**, endpoints like sign-up or password-reset get abused immediately when shipped without limits. Add per-IP rate limiting before going live (Vercel KV + a sliding window is the canonical pattern).

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←Server Components vs Client Components
Back to React and Next.js
Connecting Supabase: Database and Auth→