A practical guide to choosing between Route Handlers and Server Actions in Next.js App Router. Covers migration from Pages Router API routes, real-world decision criteria, and when each approach shines.
Mental model:
Route Handlers → Public HTTP endpoints (machines call you)
Server Actions → Server functions invoked from React (humans via UI)
Use app/api/.../route.ts when you need a real HTTP endpoint:
External services call your app
Webhooks (Sanity, Stripe, GitHub, etc.)
OAuth callbacks (e.g. /api/auth/callback/...)
Any third-party integration that sends HTTP requests
You’re exposing an API
GraphQL endpoint (e.g. /api/graphql)
REST endpoints for mobile apps or other backends
Any public or partner API surface
You need low-level HTTP control
Custom status codes and headers
CORS configuration
Streaming responses
File downloads / binary responses
Examples from the content:
Sanity webhook: verifies a secret header, parses JSON, revalidates cache tags, and returns JSON with status codes.
GraphQL endpoint: exposes a standard HTTP interface with CORS and GraphiQL support.
Rule: If something outside your React tree or outside your app must call it via URL → Route Handler.
Use 'use server' functions when React components need to run server-side logic:
Triggered by UI / React
Form submissions (<form action={myAction}>)
Button clicks (onClick={async () => await myAction()})
Any user interaction that causes a mutation
They perform mutations
Create / update / delete records
Toggle flags or modes (e.g. draft/preview mode)
Send emails, log events, etc.
You want simplicity and type safety
No manual fetch() boilerplate
Direct function calls instead of URLs
Works naturally with useTransition and React’s async patterns
Example from the content:
Disable draft mode: a Server Action that calls draftMode().disable() and is invoked directly from a client component button, followed by router.refresh().
Rule: If a human in your UI triggers it and it doesn’t need to be a public HTTP endpoint → Server Action.
Disabling draft mode:
Route Handler version: /api/draft-mode/disable + manual redirect/refresh handling.
Server Action version: disableDraftMode() called directly from a button → cleaner, type-safe, no URL management.
In this case, Server Action is better because the trigger is purely UI-driven and no external system needs the endpoint.
Overusing Route Handlers for internal UI forms
You end up writing fetch('/api/...'), handling JSON, and duplicating types.
Prefer Server Actions: <form action={submitContact}> and let Next.js handle the plumbing.
Trying to use Server Actions for webhooks or third-party callbacks
External services cannot call a Server Action directly.
They need a URL → use a Route Handler.
Choose a Route Handler if:
[ ] A third-party service must call it via HTTP
[ ] You’re building a public or partner API
[ ] You need CORS, custom headers, status codes, or streaming
Choose a Server Action if:
[ ] It’s only called from your React components
[ ] It’s a mutation triggered by a user interaction
[ ] You want to avoid manual fetch() and keep things type-safe and simple
Short rule of thumb:
Human via UI → Server Action
Machine via HTTP → Route Handler