Use a Server Action when a form or button inside your Next.js application performs an app-owned mutation. Use a Route Handler when another system needs a stable HTTP endpoint: a webhook, mobile app, public API, health check, or file response.

Neither option is automatically secure. Next.js recommends authenticating and authorizing inside every Server Action and treating Route Handlers like public API endpoints.

Decision table

Requirement Server Action Route Handler
Form submitted by your React UI Usually best Works, but adds manual fetch code
Stripe or GitHub webhook No Yes
Mobile or third-party client No Yes
Explicit HTTP method and status Limited abstraction Full control
Return JSON, XML, image, or file Not the main use Yes
Revalidate UI after a mutation Convenient Manual client flow
Progressive-enhancement form Strong fit Requires client request logic

The deciding question is who calls the code. If the caller is your rendered app, a Server Action can remove boilerplate. If the caller only knows HTTP, use a Route Handler.

Server Action example: update a profile

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

import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { requireUser } from '@/lib/auth';
import { db } from '@/lib/db';

const inputSchema = z.object({
  displayName: z.string().trim().min(2).max(60)
});

export async function updateProfile(formData: FormData) {
  const user = await requireUser();
  const input = inputSchema.parse({
    displayName: formData.get('displayName')
  });

  await db.user.update({
    where: { id: user.id },
    data: input
  });

  revalidatePath('/settings');
}
// app/settings/profile-form.tsx
import { updateProfile } from './actions';

export function ProfileForm() {
  return (
    <form action={updateProfile}>
      <label htmlFor="displayName">Display name</label>
      <input id="displayName" name="displayName" required />
      <button type="submit">Save</button>
    </form>
  );
}

The action is close to the UI, but the authorization check still belongs inside the action. Hiding the button from unauthorized users is not protection.

Route Handler example: receive a webhook

A webhook caller needs a URL, request body, headers, and a meaningful status code.

// app/api/webhooks/payments/route.ts
import { headers } from 'next/headers';
import { verifyPaymentEvent } from '@/lib/payments';
import { processPaymentEvent } from '@/lib/payment-events';

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

  if (!signature) {
    return Response.json({ error: 'missing signature' }, { status: 400 });
  }

  const event = verifyPaymentEvent(rawBody, signature);
  await processPaymentEvent(event);

  return new Response(null, { status: 204 });
}

Reading the raw body before JSON parsing is important for providers that sign exact request bytes. The handler can return 400, 401, 409, or 204 according to the HTTP contract.

Route Handlers are better for shared contracts

Choose a Route Handler when you need one or more of these:

  • A mobile app or external integration calls the endpoint.
  • The endpoint is documented with OpenAPI.
  • Consumers depend on status codes and response headers.
  • The route returns a non-UI format such as JSON, XML, or a file.
  • A webhook provider retries requests.
  • You need standard HTTP caching behavior.

A Route Handler can still call the same service function used by a Server Action. Keep business logic below the transport layer instead of duplicating it.

// lib/users/update-display-name.ts
export async function updateDisplayName(userId: string, name: string) {
  return db.user.update({
    where: { id: userId },
    data: { displayName: name }
  });
}

Now the web form and API can share validation and domain behavior while keeping different request contracts.

Security rules for both

Risk Required control
User changes another user’s data Authorize the resource, not only the session
Invalid form or JSON input Parse with a schema on the server
Duplicate webhook delivery Use an event ID or idempotency key
Secret leaks in errors Return a safe message; log details server-side
Expensive operation abuse Add rate limits and workload limits
Cross-origin Server Action use Keep allowed origins narrow

Server-side execution does not make client input trustworthy. Every identifier, field, and permission must still be checked.

A common architecture mistake

Do not put database logic directly in five Server Actions and three Route Handlers. That creates eight slightly different implementations of the same rule.

Use this shape:

UI form -> Server Action -> application service -> database
Webhook -> Route Handler -> application service -> database
Mobile app -> Route Handler -> application service -> database

The transport validates request-specific details. The application service enforces the business rule. This also makes unit tests independent of Next.js request objects.

Practical choice

Use Server Actions for app-owned form mutations where their UI integration saves code. Use Route Handlers whenever the HTTP contract is part of the feature. If both callers need the same operation, share a service underneath them.

Official references