BiTree
  • Search For Lessons
  • Curriculum
  • Pricing
  • For Educators
  • Become a Tutor
  • 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 →
BiTree

Live coding lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Search For Lessons
  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Become a Tutor
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 BiTree. 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→