Next.js API Routes
Build API endpoints in Next.js with route handlers that read requests and return JSON responses.
TL;DR
- Create an endpoint by exporting
GETorPOSTfromroute.ts. - Read a JSON request body with
await request.json()in a handler. - Return data with
Response.json()and set a status code.
Creating Handlers
GETExport a function named GET to respond to read requests at this path.
// app/api/posts/route.ts
export async function GET() {
const posts = await getPosts();
return Response.json(posts);
}POSTRead a JSON body and return a 201 status after creating a record.
export async function POST(request: Request) {
const body = await request.json();
const post = await createPost(body);
return Response.json(post, { status: 201 });
}DELETEExport PUT, PATCH, or DELETE from the same file to handle more methods.
export async function DELETE() {
await clearPosts();
return new Response(null, { status: 204 });
}Reading the Request
searchParamsRead query string values from the request URL with nextUrl.searchParams.
import type { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const q = request.nextUrl.searchParams.get('q');
return Response.json({ q });
}paramsAwait the dynamic segment values from the second argument of the handler.
// app/api/posts/[id]/route.ts
export async function GET(
_req: Request,
ctx: RouteContext<'/api/posts/[id]'>
) {
const { id } = await ctx.params;
return Response.json({ id });
}cookies() and headers()Read cookies and headers with the async helpers from next/headers.
import { cookies, headers } from 'next/headers';
export async function GET() {
const token = (await cookies()).get('token');
const agent = (await headers()).get('user-agent');
return Response.json({ agent, signedIn: !!token });
}formData()Read fields from a submitted HTML form instead of a JSON body.
export async function POST(request: Request) {
const form = await request.formData();
const email = String(form.get('email'));
return Response.json({ email });
}Sending Responses
Response.json()Return JSON and set the status code with the second argument.
return Response.json(
{ error: 'Not found' },
{ status: 404 }
);HeadersSet response headers through the options object of a Response.
return new Response('ok', {
headers: {
'Content-Type': 'text/plain',
'Cache-Control': 'no-store',
},
});Set a CookieWrite a cookie from a handler with the cookies() helper.
import { cookies } from 'next/headers';
export async function POST() {
(await cookies()).set('theme', 'dark');
return Response.json({ ok: true });
}redirect()Send the caller to another URL from inside a handler.
import { redirect } from 'next/navigation';
export async function GET() {
redirect('/login');
}Caching and Limits
Cached HelperCache the data instead of the handler by moving the query into a cached function.
async function getPosts() {
'use cache';
return sql`SELECT * FROM posts`;
}
export async function GET() {
return Response.json(await getPosts());
}force-staticPrerender a GET handler at build time in apps without Cache Components.
export const dynamic = 'force-static';
export async function GET() {
return Response.json({ version: '1.0' });
}maxDurationRaise the time limit for a slow handler on platforms that support it.
export const maxDuration = 30; // secondsTips
- Use a Server Component or Server Action for your own pages, and keep
route.tshandlers for webhooks and outside clients. - Type dynamic handlers with the global
RouteContexthelper, soparamsalways matches the folder names in the path.
Warnings
- A folder cannot hold both
route.tsandpage.tsx, so put API handlers under a separate path likeapp/api. - A
GEThandler is not cached by default, so every request runs your code unless you opt in to caching.
In Practice
Build one route file that lists posts and creates a new one with a basic check.
- GET reads an optional limit from the query string and returns the posts.
- POST reads the JSON body, because the client sends the new post as JSON.
- A missing title returns a 400 status, so the caller knows what went wrong.
- A successful create returns a 201 status with the saved record.
// app/api/posts/route.ts
import type { NextRequest } from 'next/server';
import { sql } from '@/lib/db';
export async function GET(request: NextRequest) {
const limit = request.nextUrl.searchParams.get('limit');
const posts = await sql`
SELECT * FROM posts LIMIT ${Number(limit ?? 10)}
`;
return Response.json(posts);
}
export async function POST(request: NextRequest) {
const { title } = await request.json();
if (!title) {
return Response.json(
{ error: 'Title is required' },
{ status: 400 }
);
}
const [post] = await sql`
INSERT INTO posts (title) VALUES (${title}) RETURNING *
`;
return Response.json(post, { status: 201 });
}FAQ
Add a route.ts file inside the app folder, for example app/api/posts/route.ts. Export an async function named after an HTTP method, such as GET or POST. Return a Response.
They do the same job. API routes are the Pages Router version in pages/api. Route handlers are the App Router version in route.ts files, and they use the standard web Request and Response objects.
Use a Server Action for forms and mutations inside your own app. Use a route handler when something outside your app calls it, such as a webhook, a mobile app, or a public API.
Use request.nextUrl.searchParams on a NextRequest. For example, request.nextUrl.searchParams.get('q') reads the value of ?q=.