Skip to main content
Alvin QuachFull Stack Developer
HomeProjectsExperienceBlog
HomeProjectsExperienceBlog
alvinquach

Full Stack Developer building systems that respect complexity.

Open to opportunities

AQ

Projects

  • All Projects
  • Hoparc Physical Therapy
  • OpportunIQ
  • Hoop Almanac
  • SculptQL

Knowledge

  • Blog
  • Experience
  • Interview Prep

Connect

  • Contact
  • LinkedIn
  • GitHub
  • X

Resources

  • Resume
© 2026All rights reserved.
Back to Blogs
Decision
Featured
Depth: ●●○○○

Route Handlers vs Server Actions: When to Use Each in Next.js App Router

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.

Published August 13, 20253 min readImportance: ★★★★★
Share:

Key Takeaways: Route Handlers vs Server Actions in Next.js App Router

Mental model:

Route Handlers → Public HTTP endpoints (machines call you)

Server Actions → Server functions invoked from React (humans via UI)

When to Use Route Handlers

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.

When to Use Server Actions

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.

Side-by-Side: Same Feature, Different Tool

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.

Common Pitfalls

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.

Practical Decision Checklist

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