DEV Community

Ahmed Mahmoud
Ahmed Mahmoud

Posted on • Originally published at devya.dev

Route Handlers vs Server Actions in Next.js: When I Reach for Each

Headline: In the Next.js App Router, a Server Action is an async function you call directly from a component to run a mutation, and a Route Handler is a standard HTTP endpoint at app/api/**/route.ts. Reach for a Server Action for mutations driven by your own React UI; reach for a Route Handler for public APIs, webhooks, and streaming. Both compile to publicly callable endpoints, so authenticate inside each one.

Key takeaways

  • A Server Action is an async function marked 'use server' that Next.js exposes as an internal POST endpoint you invoke directly from a component — usually a form action.
  • A Route Handler is a function exported from app/api/**/route.ts named after an HTTP method that receives a Web Request and returns a Web Response.
  • Use a Server Action for mutations from your own UI — progressive enhancement plus revalidateTag in the same round trip.
  • Use a Route Handler for public APIs, webhooks, OAuth callbacks, cacheable GET endpoints, and streaming.
  • Both are public endpoints — authenticate, authorize, and validate inputs inside every one.

What is a Server Action vs a Route Handler in Next.js?

A Server Action is an async function marked with the 'use server' directive that Next.js compiles into an RPC-style endpoint. You import it and call it like a normal function — most often as the action of a <form> — and Next.js serializes the call over a POST request behind the scenes.

A Route Handler is a function exported from a route.ts file inside app/, named after the HTTP method it serves (GET, POST, PUT, PATCH, DELETE). It receives a standard Web Request and returns a standard Web Response, so you control the status code, headers, and body directly. It replaces pages/api.

When should I use a Server Action instead of a Route Handler?

Use a Server Action when the mutation is triggered from your own React UI and has no external consumer. A form that updates a profile, a button that deletes a row — these belong in Server Actions because the action lives next to the component that calls it.

Server Actions give you progressive enhancement (a <form action={fn}> works before JS hydrates) and in-place cache revalidation (revalidateTag re-renders affected Server Components in the same round trip).

// app/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
import { auth } from '@/lib/auth';

export async function updateName(formData: FormData) {
  const session = await auth();                 // authenticate INSIDE the action
  if (!session) throw new Error('Unauthorized');

  const name = String(formData.get('name') ?? '').trim();
  if (!name) return { error: 'Name is required' };

  await db.user.update({ where: { id: session.userId }, data: { name } });
  revalidateTag('profile');
  return { ok: true };
}
Enter fullscreen mode Exit fullscreen mode

When should I use a Route Handler instead of a Server Action?

Use a Route Handler when something outside your React tree calls the endpoint over a stable URL: webhook receivers (Stripe, GitHub), OAuth callbacks, mobile or third-party API consumers, cron jobs, and cacheable GET endpoints.

Route Handlers are also the only option for raw request access or streaming. A Stripe webhook needs the unparsed body to verify the signature; an SSE feed or LLM token stream returns a ReadableStream. Server Actions return a single serialized value.

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';

export async function POST(req: Request) {
  const body = await req.text();                        // raw body for signature check
  const sig = (await headers()).get('stripe-signature')!;
  let event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WH_SECRET!);
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }
  return Response.json({ received: true });
}
Enter fullscreen mode Exit fullscreen mode

How do Route Handlers and Server Actions compare?

Aspect Server Action Route Handler
How it's called Imported and called like a function HTTP request to a URL
HTTP method Always POST (internally) Any (GET/POST/PUT/PATCH/DELETE)
External clients No stable public contract Yes — stable URL + method
Progressive enhancement Yes (form works without JS) No
Cacheable GET No Yes (Cache-Control)
Streaming No Yes (ReadableStream)
Cache revalidation revalidateTag inline Manual
Best for Mutations from your own UI Public APIs, webhooks, streaming

Do I need a Route Handler to fetch data for my own page?

No. Inside a Server Component you query your database directly — no Route Handler required. Wrapping the query in app/api and fetching it from the same server just adds an HTTP round trip and a second cache layer for no benefit.

// app/dashboard/page.tsx — no Route Handler, no fetch()
export default async function Page() {
  const stats = await db.stats.find();
  return <Stats data={stats} />;
}
Enter fullscreen mode Exit fullscreen mode

Reserve Route Handler GETs for data a different client needs: a public JSON API, an edge-cached response, or an endpoint a mobile app calls.

Are Server Actions and Route Handlers both public endpoints?

Yes — both are publicly reachable POST endpoints. A Server Action is not private just because you call it from a component and never see its URL. Next.js compiles each action into an endpoint with an unguessable ID, but that ID can be invoked directly with a crafted request. The action ID is obscurity, not authorization.

Treat every Server Action and Route Handler as an untrusted entry point: authenticate the caller, check authorization, and validate every input inside the function body. The client UI enforces nothing.

FAQ

Q: Can a Server Action handle a GET request?
A: No. Server Actions always execute as POST. For a cacheable GET endpoint, use a Route Handler exporting a GET function.

Q: Should I use a Route Handler to fetch data for my own page?
A: No. In a Server Component, query your data source directly. A Route Handler adds an extra HTTP round trip and cache layer for no benefit.

Q: Are Server Actions secure by default because they have no visible URL?
A: No. Every Server Action compiles to a callable POST endpoint. Authenticate, authorize, and validate inputs inside the action itself.

Q: Which one supports streaming responses?
A: Route Handlers. Return a ReadableStream for SSE or LLM token streams. A Server Action returns one serialized result.

Q: Can a mobile app or third-party service call my Server Actions?
A: Not reliably. Server Action IDs are generated at build time and are not a stable public contract. Expose a Route Handler with a versioned URL.


Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.

Top comments (1)

Collapse
 
synfinity-dynamics-pvt-ltd profile image
Synfinity Dynamics Pvt Ltd

Great comparison. The reminder that both Server Actions and Route Handlers are public entry points is especially important.