Frontend Jul 7, 2026 · 7 min read

Server Actions in Next.js: where they shine and where I refuse to use them

Yohangel Ramos

Yohangel Ramos

Tech Lead · Senior Fullstack Developer

Next.js Server Actions are the framework's best and worst idea, depending on where you put them. For form mutations — create, edit, delete from the UI — they eliminate an entire layer of boilerplate: no endpoint to define, no manual serialization, no fetch or loading states managed with useEffect. But I've seen (and had to undo) projects where they became the only way to talk to the server, and there the pattern breaks: business logic trapped in the framework, impossible to consume from another client, and an invalidation graph nobody understands. My criteria after using them in production: Server Actions are the UI's interaction layer, not your product's API. This article is the line I draw and why.

Where they shine: the classic form mutation

The happy case is a form that mutates data and revalidates the view. With an action, shared Zod validation, and React's own pending state:

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

import { revalidatePath } from 'next/cache';
import { projectSchema } from '@/schemas/project';

export async function createProject(formData: FormData) {
  const session = await getSession();
  if (!session) throw new Error('Unauthorized');

  const parsed = projectSchema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors };
  }

  await prisma.project.create({
    data: { ...parsed.data, ownerId: session.userId },
  });

  revalidatePath('/projects');
  return { ok: true };
}

Zero endpoints, zero HTTP client, the return type travels inferred all the way to the component. For 80% of the mutations in an internal dashboard, this is all you need, and going back to writing route handlers for those cases is nostalgia, not engineering.

The red line: business logic living in the action

The problem starts when the action stops being an adapter and becomes the home of the logic. An action that calculates prices, orchestrates three services, and decides business rules is code that can only be invoked from a React component in that Next.js project. The day the mobile app arrives, or the cron job, or the team that wants a public endpoint, that logic has to be exhumed.

My structural rule: the action calls a service, and the service doesn't know Next.js exists.

// services/projects.ts — framework-agnostic, testable in isolation
export async function createProjectForUser(
  input: ProjectInput,
  userId: string
) {
  // all business logic lives here
}

The action ends up as three lines: authenticate, parse, delegate. If tomorrow I need to expose the same thing via a route handler or an SQS worker, the service is already there. I chose this separation knowing it duplicates a bit of ceremony; in exchange, no business decision is held hostage by the framework.

💡 A Server Action is a transport mechanism with syntactic sugar, not an architecture layer. If deleting Next.js from your project takes business logic with it, the logic was in the wrong place.

The operational limits nobody tells you about until production

They are sequential POSTs. Server Actions execute serially by default from the same client: if the user fires three, they queue. For a form it doesn't matter; for high-frequency interactions (autosave, drag and drop on a board) it's a bottleneck you discover late.

They are neither cancelable nor cacheable. A GET to a route handler can be CDN-cached and aborted with AbortController. An action cannot. Anything that's a read — search, autocomplete, filters — has no business being in an action: that's a route handler or directly a Server Component reading from the database.

Error handling is opaque. A throw in an action reaches the client as a generic error in production (by design, to avoid leaking internals). That forces you to model expectable errors as return values — the { error } in the example — and reserve throw for the truly exceptional. It's more Result than Exception, and it's worth deciding on day one so you don't mix styles.

The criteria in one sentence

A mutation initiated by a user from that same project's UI: Server Action delegating to a service. Reads, APIs consumed by third parties, webhooks, high frequency, anything that might one day need another client: route handler or a proper backend. With that line drawn, Server Actions are an excellent tool — small, boring, and in their place, which is exactly how I like my tools.

Yohangel Ramos

Written by Yohangel Ramos

Senior Fullstack Developer and Tech Lead. I build with React, Next.js, Nest.js and AWS — and I write about what I learn along the way.

Let's talk →

Keep reading

IA

AI-built startups in 2026: the real numbers behind the hype

IA

How to survive as a programmer in the AI era (without turning cynical or naive)