Route Handlers: building an API endpoint
Every route so far has rendered a page — HTML, from a React component. A Route Handler is the App Router's way of building an endpoint that returns something else entirely, JSON most commonly, the same job a REST framework's endpoint does.
A minimal GET handler
// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { getAllPosts } from "@/lib/posts";
export async function GET() {
const posts = getAllPosts();
return NextResponse.json(posts);
}route.ts is the reserved filename for a Route Handler, the API equivalent of page.tsx for a page — and the two can't coexist in the same folder, since a segment is either a page or an API route, not both. app/api/posts/route.ts maps to /api/posts, following the exact same folder-to-URL mapping every page route in this series has used. Exporting a function named after an HTTP method — GET here — is what Next.js calls for a request using that method; visiting /api/posts in a browser (a GET request) returns the JSON array directly.
Handling more than one method
// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { getAllPosts, createPost } from "@/lib/posts";
export async function GET() {
return NextResponse.json(getAllPosts());
}
export async function POST(request: Request) {
const body = await request.json();
const post = createPost(body);
return NextResponse.json(post, { status: 201 });
}Same file, one export per method — no branching on request.method by hand the way an older-style API route required. request.json() parses the request body; NextResponse.json(post, { status: 201 }) mirrors the same "201 for a created resource" convention any REST API should follow.
Reading query parameters
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const tag = request.nextUrl.searchParams.get("tag");
const posts = tag ? getAllPosts().filter((p) => p.tags?.includes(tag)) : getAllPosts();
return NextResponse.json(posts);
}NextRequest (Next.js's extension of the standard Request) exposes .nextUrl.searchParams directly — /api/posts?tag=nextjs reads tag off it without manually constructing a URL object first, the way parsing a plain Request's URL string normally requires.
Why not just call getAllPosts() directly from a Server Component?
Every page in this series so far has done exactly that — no API layer needed for the blog's own pages, since a Server Component can call lib/posts.ts functions directly, with no network request at all involved. A Route Handler earns its place when something other than this app's own Server Components needs the data: a separate mobile app, a third-party integration, or a webhook (a payment provider, a CMS) needing an endpoint to call into.
Building a Route Handler for data a page's own Server Component could just fetch directly, out of habit from an architecture where the frontend and backend are always separate. Inside this project, a Server Component calling lib/posts.ts directly is strictly simpler — no serialization, no network round trip — than that same component fetching its own app's API over HTTP.
Next: mutating data from a form without a Route Handler at all — Server Actions, a different pattern entirely for the case a Route Handler doesn't fit.