NextjsRoute Handlers (API Routes)

Route Handlers (API Routes)

Route Handlers are the App Router's way of building API endpoints. Instead of returning JSX, a route.ts file exports one function per HTTP method — GET, POST, PUT, PATCH, DELETE, and so on — and Next.js wires each one up to that route's URL.

Note
A route segment can have either a page.tsx or a route.ts, but not both at the same level — a route.ts defines an API endpoint at that URL, and it would conflict with a page trying to render UI at the same path.
A basic GET handler

TSX
// app/api/posts/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
  })

  return NextResponse.json(posts)
}

This creates a real HTTP endpoint at /api/posts. It can be called from a Client Component with fetch, from an external service, from a mobile app, or from any other client that can make an HTTP request — unlike a Server Action, which is only callable from within your own Next.js app.

Reading dynamic segments, query params, and the request body

TSX
// app/api/posts/[slug]/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(
  request: NextRequest,
  { params }: { params: { slug: string } },
) {
  const post = await db.post.findUnique({ where: { slug: params.slug } })

  if (!post) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 })
  }

  return NextResponse.json(post)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const searchParams = request.nextUrl.searchParams
  const draft = searchParams.get('draft') === 'true'

  const post = await db.post.create({
    data: { title: body.title, content: body.content, draft },
  })

  return NextResponse.json(post, { status: 201 })
}
Caching behavior

GET Route Handlers can be cached similarly to fetch, using the same route segment configuration exports. By default, a GET handler with no dynamic APIs (cookies, headers, request.url search params) used inside it can be statically evaluated; using any of those, or exporting dynamic = 'force-dynamic', makes it render per request.

TSX
export const revalidate = 3600 // cache this GET handler's response for up to an hour
Route Handlers vs. Server Actions

Both let server-side code run in response to something the client does, but they solve different problems. Server Actions are the natural choice for mutations triggered from your own app's forms and components. Route Handlers are the natural choice whenever you need a real, addressable HTTP endpoint.

Route Handlers

Server Actions

Callable by

Any HTTP client (browser, mobile app, third party, webhook)

Only your own Next.js app

Defined in

route.ts, one export per HTTP method

A function marked "use server"

Typical use case

Public/external APIs, webhooks, custom auth callbacks, streaming responses

Form submissions, mutations from your own components

Invocation from a form

Requires manual fetch + client-side wiring

Pass directly as a form’s action prop

Returns

A Response/NextResponse object you construct

Whatever value your function returns, used directly in React

Tip
If you are building a page or component that submits a form or triggers a mutation from within your own app, prefer a Server Action — it's less code and integrates directly with useFormStatus/useActionState. Reach for a Route Handler when something outside your React tree — a webhook, a third-party integration, a public API consumer — needs to call your server.
Common use cases for Route Handlers
  • Webhooks from third-party services (payment providers, CMS platforms) that POST to a fixed URL.

  • A public API that external clients or a separate frontend consume.

  • OAuth or auth-provider callback URLs that need to run custom server logic.

  • Streaming responses, such as server-sent events or proxying a streaming AI response.

  • Generating non-HTML output at a URL, like an RSS feed, a sitemap, or an image.

Warning
Route Handlers run only on the server, but anything they return is visible to whoever can call the endpoint. Treat them like any public API: validate input, check authentication/authorization, and don't assume the caller is your own frontend.