Route Handlers
Building API endpoints in the App Router with route handlers
What you'll learn
Create GET, POST, PUT, DELETE handlers in app/api
Use Web API Request/Response standards
Build dynamic route handlers with params
Protect API routes with middleware
Stream responses from route handlers
Basic Route Handler
Route handlers live in route.ts files inside app/api/:
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: "Hello World" })
}
export async function POST(request: Request) {
const body = await request.json()
return Response.json({ received: body }, { status: 201 })
}
Dynamic Route Handlers
Access route params and search params:
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const post = await db.query("SELECT * FROM posts WHERE id = $1", [id])
if (!post) {
return Response.json({ error: "Not found" }, { status: 404 })
}
return Response.json(post)
}
Response Formats
Full CRUD Example
// app/api/todos/route.ts
const todos: Array<{ id: number; text: string; done: boolean }> = []
let nextId = 1
export async function GET() {
return Response.json(todos)
}
export async function POST(request: Request) {
const { text } = await request.json()
const todo = { id: nextId++, text, done: false }
todos.push(todo)
return Response.json(todo, { status: 201 })
}
// app/api/todos/[id]/route.ts
export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const body = await request.json()
const index = todos.findIndex((t) => t.id === Number(id))
if (index === -1)
return Response.json({ error: "Not found" }, { status: 404 })
todos[index] = { ...todos[index], ...body }
return Response.json(todos[index])
}
export async function DELETE(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const index = todos.findIndex((t) => t.id === Number(id))
if (index === -1)
return Response.json({ error: "Not found" }, { status: 404 })
const deleted = todos.splice(index, 1)[0]
return Response.json(deleted)
}
Middleware for API Protection
// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
const apiKey = request.headers.get("x-api-key")
if (
request.nextUrl.pathname.startsWith("/api/admin") &&
apiKey !== process.env.ADMIN_KEY
) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
}
export const config = {
matcher: "/api/:path*",
}
See It In Action
Build a CRUD API for Todos
Code
tsx
1// app/api/todos/route.ts2const todos: Array<{ id: number; text: string; done: boolean }> = []3let nextId = 14 5export async function GET() {6return Response.json(todos)7}8 9export async function POST(request: Request) {10const { text } = await request.json()11const todo = { id: nextId++, text, done: false }12todos.push(todo)13return Response.json(todo, { status: 201 })14}GET returns the full todos array as JSON. POST parses the request body, creates a new todo with a unique ID (auto-incrementing nextId), pushes it to the in-memory array, and responds with the created todo and a 201 status code.
Lesson Summary
Key Takeaways
Route handlers use standard Web API Request/Response objects
Dynamic routes use [param] folders with params prop
Support GET, POST, PUT, DELETE, PATCH, and more HTTP methods
Streaming responses use ReadableStream with text/event-stream
Middleware can protect API routes before they execute