EdgeCases Logo
Apr 2026
Next.js
Deep
8 min read

Next.js Route Handlers vs Server Actions

Use Server Actions for mutations called from your React components. Use Route Handlers when external clients need to call your API.

nextjs
route-handlers
server-actions
api
mutations
app-router
forms

The pattern is familiar: create a POST endpoint in app/api/, make a fetch() call from the client, handle loading states, parse JSON responses. This is Route Handler mode. But Next.js 13+ introduced Server Actions—a paradigm shift that eliminates boilerplate for internal mutations. The key distinction: Route Handlers are for external APIs; Server Actions are for React components.

Route Handlers: The External API Pattern

Route Handlers (formerly API Routes) live in app/api/[route]/route.ts and define HTTP endpoints. They're the right choice when:

  • External clients need to call your backend: Mobile apps, third-party integrations, webhooks
  • You need custom HTTP methods: PATCH, DELETE, non-standard verbs
  • Response format matters: Streaming responses, custom status codes, non-JSON responses
  • Framework-agnostic access: Services that shouldn't depend on Next.js internals
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await db.users.delete(params.id);

  return NextResponse.json({ success: true }, { status: 200 });
}

Clients call this endpoint with standard fetch():

// Client component
async function deleteUser(id: string) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'DELETE'
  });
  const data = await response.json();
  // Handle data...
}

This pattern introduces boilerplate: manual fetch(), loading states, error handling, response parsing. For internal React components calling your own backend, this overhead is unnecessary.

Server Actions: The React-First Pattern

Server Actions are functions that execute on the server but can be called directly from React components. They eliminate the HTTP abstraction layer for mutations. Use them when:

  • React components need to mutate data: Form submissions, button clicks, user interactions
  • You want automatic loading states: useTransition() for pending UI
  • Progressive enhancement matters: Forms work without JavaScript
  • You're building internal APIs: Only called by your Next.js app
// app/actions.ts
'use server';

import { db } from '@/lib/db';

export async function deleteUser(formData: FormData) {
  const id = formData.get('id') as string;
  await db.users.delete(id);

  revalidatePath('/users');
}

Call the Server Action directly from a component:

// Client component
import { deleteUser } from '@/app/actions';

function UserList() {
  return (
    
  );
}

No fetch(). No manual async/await. No response parsing. Server Actions handle the client-server bridge automatically.

The Performance Difference

Route Handlers incur full HTTP request/response cycle overhead:

  1. Client makes HTTP request (network latency)
  2. Request goes through Next.js routing middleware
  3. Route Handler executes
  4. Response serialized and sent back
  5. Client parses response

Server Actions use an optimized path: they're serialized once at build time into internal endpoints that bypass full HTTP routing. For mutations triggered by React components, Server Actions are 10-30% faster due to reduced serialization overhead.

Loading States and Errors

Server Actions integrate with React 18's concurrent features for better UX:

// Client component
import { useTransition } from 'react';
import { deleteUser } from '@/app/actions';

function UserList() {
  const [isPending, startTransition] = useTransition();

  return (
    
  );
}

With Route Handlers, you manage this manually:

// Manual loading state management
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState

Server Actions reduce 50+ lines of boilerplate for each mutation when you account for loading states, errors, and response handling.

Form Handling: Progressive Enhancement

Server Actions with <form> work without JavaScript—critical for progressive enhancement. If JavaScript fails to load or is blocked, forms still submit using standard browser form submission:

// Works with and without JavaScript

Route Handlers require JavaScript for all mutations—event.preventDefault(), fetch(), and response handling all depend on client-side JS. This is a progressive enhancement failure.

When to Use Route Handlers

Despite the benefits of Server Actions, Route Handlers remain essential for:

  • Webhooks: Stripe, GitHub, external services push data to your endpoints
  • Third-party integrations: Zapier, IFTTT, custom API clients
  • Streaming responses: Server-Sent Events, real-time data, large file transfers
  • Custom authentication: OAuth callbacks, JWT validation, session management
  • CORS requirements: External origins need to call your API
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  const signature = request.headers.get('stripe-signature');
  const body = await request.text();

  const event = stripe.webhooks.constructEvent(
    body,
    signature!,
    process.env.STRIPE_WEBHOOK_SECRET!
  );

  // Handle webhook event...
  return NextResponse.json({ received: true });
}

This pattern cannot be replaced by Server Actions—webhooks require standard HTTP endpoints that external systems call.

Common Pitfalls

Pitfall 1: Using Server Actions for external APIs

Mobile apps or third-party services can't call Server Actions directly—they're React-specific. Build Route Handlers for any API surface consumed outside your Next.js app.

Pitfall 2: Ignoring data fetching

Server Actions are optimized for mutations, not data fetching. Use Server Components, fetch() in RSCs, or Route Handlers for GET requests. Server Actions can fetch data, but they're not designed for read-only operations.

Pitfall 3: Missing revalidation

Server Actions don't automatically invalidate cached data. Use revalidatePath() or revalidateTag() to update stale data after mutations:

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function updateUser(formData: FormData) {
  const id = formData.get('id') as string;
  await db.users.update(id, formData);

  revalidatePath('/users'); // Invalidate /users cache
  revalidateTag('users'); // Invalidate all users-tagged data
}

Decision Framework

Use this quick guide to choose between Route Handlers and Server Actions:

Use CaseBest ChoiceWhy
Form submission from ReactServer ActionProgressive enhancement, automatic loading
Button click mutationServer ActionNo fetch boilerplate
External webhookRoute HandlerStandard HTTP endpoint required
Mobile app APIRoute HandlerFramework-agnostic access
Streaming responseRoute HandlerFull HTTP response control
Data fetching (GET)Server ComponentNative RSC data fetching

The rule of thumb: Route Handlers for external APIs, Server Actions for React mutations. Don't default to one or the other—match the tool to the use case.

Advertisement

Related Insights

Explore related edge cases and patterns

Next.js
Deep
Server Components Data Fetching Patterns
7 min
Next.js
Expert
Partial Prerendering: Streaming + Static Shell
8 min
Next.js
Deep
Next.js Server Actions Error Handling Patterns
9 min
Next.js
Deep
Next.js Server Actions Error Handling Patterns
9 min

Advertisement