Suggest an editImprove this articleRefine the answer for “API routes (route handlers) in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Route Handlers** in Next.js are `route.ts` files inside the `app` directory that export named HTTP method functions to create server-side API endpoints. ```tsx // app/api/users/route.ts export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) } ``` **Key point:** use Route Handlers for public APIs and webhooks; use Server Actions for mutations triggered from your own UI.Shown above the full answer for quick recall.Answer (EN)Image**Route Handlers** are `route.ts` files inside the `app` directory that export named HTTP method functions and act as server-side API endpoints in a Next.js app. ## Theory ### TL;DR - A `route.ts` file exports named functions (`GET`, `POST`, `DELETE`, etc.) that map directly to HTTP methods - By convention they live under `app/api/`, but any subfolder inside `app` works - GET handlers that don't read the request get cached automatically; everything else runs dynamically - Use Route Handlers for public APIs, webhooks, and external integrations - Use Server Actions when the mutation comes from a form or button in your own UI ### Quick example ```tsx // app/api/users/route.ts import { NextResponse } from 'next/server' import { db } from '@/lib/db' export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) // 200 by default } export async function POST(request: Request) { const body = await request.json() const user = await db.user.create({ data: body }) return NextResponse.json(user, { status: 201 }) } ``` Two exported functions, two HTTP methods handled. That's the whole pattern. ### Dynamic routes Add `[id]` to the folder name and Next.js passes the segment as the second argument to the handler: ```tsx // app/api/users/[id]/route.ts export async function GET( request: Request, { params }: { params: { id: string } } ) { const user = await db.user.findUnique({ where: { id: params.id } }) if (!user) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } return NextResponse.json(user) } export async function DELETE( request: Request, { params }: { params: { id: string } } ) { await db.user.delete({ where: { id: params.id } }) return new Response(null, { status: 204 }) // no body on delete } ``` ### Reading request data Route Handlers use the standard Web `Request` API. No Next.js-specific abstraction, just the platform: ```tsx // app/api/search/route.ts export async function GET(request: Request) { const { searchParams } = new URL(request.url) const query = searchParams.get('q') if (!query) { return NextResponse.json({ error: 'Missing query param' }, { status: 400 }) } const results = await db.post.findMany({ where: { title: { contains: query, mode: 'insensitive' } } }) return NextResponse.json(results) } ``` For cookies and headers, use the helpers from `next/headers`: ```tsx import { cookies, headers } from 'next/headers' export async function GET() { const token = cookies().get('session')?.value const userAgent = headers().get('user-agent') return NextResponse.json({ token, userAgent }) } ``` ### Caching behavior Caching is where most developers get surprised. A GET handler with no `request` argument is treated as static and cached at build time. The moment you read anything from the request, Next.js makes the route dynamic. ```tsx // Cached - Next.js computes this at build time export async function GET() { const res = await fetch('https://api.example.com/posts') return NextResponse.json(await res.json()) } // Not cached - reads request data export async function GET(request: Request) { const { searchParams } = new URL(request.url) // runs on every request } ``` Force or opt out of caching with route segment config: ```tsx export const dynamic = 'force-dynamic' // run on every request, no cache export const revalidate = 300 // cache for 5 minutes, then re-fetch ``` ### Route Handlers vs Server Actions | | Route Handlers | Server Actions | |---|---|---| | Purpose | API endpoints | UI mutations | | HTTP methods | GET, POST, PUT, DELETE, PATCH... | POST only | | Called from | Any HTTP client (fetch, curl, mobile apps) | React components only | | Progressive enhancement | No | Yes (works with plain HTML forms) | | When to pick | Public API, webhooks, third-party integrations | Form submissions, button clicks in your own UI | If an external service needs to call your endpoint, use a Route Handler. If only your own UI triggers it, Server Actions remove a lot of boilerplate. ### Common mistakes **1. Not awaiting params in Next.js 15** Starting with Next.js 15, `params` became a Promise. Accessing it directly returns `undefined`: ```tsx // Breaks in Next.js 15 export async function GET( _: Request, { params }: { params: { id: string } } ) { console.log(params.id) // undefined in Next.js 15 } // Correct export async function GET( _: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params console.log(id) } ``` **2. Assuming GET is always dynamic** A GET handler that doesn't read `request` gets cached. If you return fresh database data each time but forget `export const dynamic = 'force-dynamic'`, users see stale results after a deploy. **3. Putting `route.ts` and `page.tsx` in the same segment** ``` app/users/page.tsx <- page app/users/route.ts <- route handler in the same folder = conflict ``` Next.js throws an error. Move the handler to `app/api/users/route.ts`. **4. Skipping error handling** ```tsx // Bad - unhandled db error becomes a 500 with an empty body export async function GET() { const users = await db.user.findMany() return NextResponse.json(users) } // Better export async function GET() { try { const users = await db.user.findMany() return NextResponse.json(users) } catch { return NextResponse.json({ error: 'Database error' }, { status: 500 }) } } ``` ### Real-world usage - Public REST APIs consumed by mobile apps or third-party clients - Webhook receivers: Stripe payments, GitHub events, Clerk auth events - OAuth callback handlers - File upload endpoints - Cron job triggers called by external schedulers (Vercel Cron, GitHub Actions) ### Follow-up questions **Q:** Can you call a Route Handler from a Server Component? **A:** You can, but it creates a pointless HTTP round-trip. Server Components already run on the server, so call the database or service directly instead. **Q:** What happens if you export a function with an unrecognized method name from `route.ts`? **A:** Next.js ignores it. Only `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS` are wired to HTTP methods. **Q:** How do you share auth logic across multiple Route Handlers without repeating it? **A:** Two options: use `middleware.ts` to protect entire route groups before the request reaches the handler, or write a wrapper function that checks the session and wrap each individual handler with it. **Q:** What is the difference between `NextResponse` and the native `Response`? **A:** `NextResponse` extends the native `Response` with helpers like `.json()`, cookie manipulation, and redirects. For simple cases, `new Response(body, { status: 200 })` works fine too. ## Examples ### Basic CRUD endpoint ```tsx // app/api/posts/route.ts import { NextResponse } from 'next/server' import { db } from '@/lib/db' export async function GET() { const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } }) return NextResponse.json(posts) } export async function POST(request: Request) { const { title, content } = await request.json() if (!title) { return NextResponse.json({ error: 'Title is required' }, { status: 400 }) } const post = await db.post.create({ data: { title, content } }) return NextResponse.json(post, { status: 201 }) } ``` GET returns posts sorted by date. POST validates input before writing to the database and returns 201 on success. ### Webhook handler with signature verification ```tsx // app/api/webhooks/stripe/route.ts import { headers } from 'next/headers' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) export async function POST(request: Request) { const body = await request.text() // raw string, not JSON const signature = headers().get('stripe-signature')! let event: Stripe.Event try { event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ) } catch { return new Response('Invalid signature', { status: 400 }) } if (event.type === 'checkout.session.completed') { // handle successful payment } return new Response(null, { status: 200 }) } ``` Stripe sends raw bytes, so you must read the body with `request.text()`. If you call `request.json()` first, the signature check fails every time. ### Protected endpoint with session validation ```tsx // app/api/profile/route.ts import { cookies } from 'next/headers' import { verifySession } from '@/lib/auth' import { NextResponse } from 'next/server' import { db } from '@/lib/db' export async function GET() { const token = cookies().get('session')?.value if (!token) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const session = await verifySession(token) if (!session) { return NextResponse.json({ error: 'Invalid session' }, { status: 401 }) } const user = await db.user.findUnique({ where: { id: session.userId } }) return NextResponse.json(user) } ``` Read the cookie, verify it, then fetch the user. Two gates before any data leaves the server.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.