Next.js API Routes
Build and handle API endpoints directly inside your Next.js application.
TL;DR
- 01Create API routes in app/api folder with route.ts files.
- 02Export handler functions for HTTP methods (GET, POST, etc).
- 03Access request data and return typed Response objects with status codes.
Tips
- 01Use "use server" functions in app/actions for simple mutations — API routes are for complex endpoints.
Warnings
- 01API routes are public by default — add authentication checks for sensitive endpoints.
Basic API Route
- Create API routes in app/api folder.
// app/api/hello/route.ts export async function GET(request: Request) { return Response.json({ message: 'Hello' }); } - Each route.ts file becomes an API endpoint.
- /app/api/hello/route.ts becomes /api/hello.
- Return a plain text response using the Response constructor.
export async function GET() { return new Response("Hello, world!", { headers: { "Content-Type": "text/plain" } }); } - Set response status and headers with the second argument.
export async function POST() { return Response.json({ created: true }, { status: 201 }); }
HTTP Methods
- Handle different HTTP methods with exports.
// app/api/posts/route.ts export async function GET() { const posts = await fetchPosts(); return Response.json(posts); } export async function POST(request: Request) { const body = await request.json(); const post = await createPost(body); return Response.json(post, { status: 201 }); } - Supports GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.
- Handle PUT requests to update an existing resource.
export async function PUT(request: Request) { const body = await request.json(); const updated = await updatePost(body.id, body); return Response.json(updated); } - Handle DELETE to remove a resource by ID.
export async function DELETE(request: Request) { const { id } = await request.json(); await deletePost(id); return Response.json({ deleted: true }); } - Return 405 for unsupported methods.
export async function PATCH() { return Response.json({ error: "Method not allowed" }, { status: 405 }); }
Request Handling
- Read JSON body from requests.
export async function POST(request: Request) { const body = await request.json(); console.log(body); return Response.json({ success: true }); } - Access query parameters and authorization headers.
export async function GET(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); const auth = request.headers.get('authorization'); return Response.json({ id, auth }); } - Parse form data from multipart requests.
export async function POST(request: Request) { const formData = await request.formData(); const name = formData.get("name") as string; return Response.json({ name }); } - Stream a large response body using ReadableStream.
export async function GET() { const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("chunk 1")); controller.close(); } }); return new Response(stream); } - Validate request body shape before processing.
const { name, email } = await request.json(); if (!name || !email) { return Response.json({ error: "name and email are required" }, { status: 400 }); }
Cookies and Headers
- Read cookies from incoming requests —
cookies()is async in Next.js 15.import { cookies } from "next/headers"; export async function GET() { const cookieStore = await cookies(); // must await in Next.js 15 const token = cookieStore.get("token")?.value; return Response.json({ token }); } - Set cookies in the response.
import { cookies } from "next/headers"; export async function POST() { const cookieStore = await cookies(); cookieStore.set("session", "abc123", { httpOnly: true }); return Response.json({ ok: true }); } - Read request headers directly from the Request object.
export async function GET(request: Request) { const auth = request.headers.get("authorization"); const contentType = request.headers.get("content-type"); return Response.json({ auth, contentType }); } - Set custom response headers.
export async function GET() { return new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } }); } - Use
next/headersheaders()to read request headers server-side (also async in Next.js 15).import { headers } from "next/headers"; const headersList = await headers(); const userAgent = headersList.get("user-agent");
Error Handling and Status Codes
- Return appropriate status codes.
export async function GET(request: Request) { try { const data = await fetchData(); return Response.json(data); } catch (error) { return Response.json( { error: 'Failed to fetch data' }, { status: 500 } ); } } - Use Response.json with status and headers.
return Response.json(data, { status: 201, headers: { 'Content-Type': 'application/json' } }); - Return 400 for invalid or missing request body fields.
export async function POST(request: Request) { const body = await request.json(); if (!body.name) { return Response.json({ error: "Name is required" }, { status: 400 }); } return Response.json({ ok: true }); } - Return 401 for unauthenticated requests.
const token = request.headers.get("authorization"); if (!token) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } - Log errors server-side before returning a generic message.
} catch (error) { console.error("API error:", error); return Response.json({ error: "Internal server error" }, { status: 500 }); }
FAQ
Create a file at app/api/[route]/route.ts and export named async functions matching HTTP methods (GET, POST, PUT, DELETE). Each function receives a Request object and must return a Response, e.g. return Response.json({ data }) or new Response(body, { status: 201 }).
Use new URL(request.url).searchParams to access query parameters, and await request.json() to parse a JSON body. For form data, use await request.formData() instead.
Name your folder with brackets, e.g. app/api/users/[id]/route.ts, then access the param via the second argument: export async function GET(request, { params }) { const { id } = await params; }.
Use Server Actions ("use server") for form submissions and simple data mutations tied to UI components — they require less boilerplate. Use API routes when you need a public HTTP endpoint, webhook receiver, or need full control over headers, status codes, and response shape.
Pass a status option to the Response constructor or Response.json(): return Response.json({ error: 'Not found' }, { status: 404 }). For errors, always set an explicit status code — omitting it defaults to 200 even on failure.