Why I Reach for Server Actions First
Server Actions changed how I think about data mutations in Next.js. Here's my mental model after using them on three production projects.
November 15, 2024
·2 min read
Server Actions landed in Next.js 14 and I was skeptical. PHP callbacks in React? Seemed like a step backward. After using them on three real projects I've changed my mind completely.
The before/after
Before Server Actions, a simple form submission looked like this:
// 1. API route
export async function POST(req: Request) {
const body = await req.json()
const validated = schema.parse(body)
await db.thing.create({ data: validated })
return Response.json({ ok: true })
}
// 2. Client component
async function handleSubmit(data: FormData) {
const res = await fetch('/api/things', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(data)),
headers: { 'Content-Type': 'application/json' },
})
if (!res.ok) throw new Error('Failed')
}Two files. Two mental contexts. A fetch call that could silently break if the endpoint moves.
With Server Actions:
// One file, colocated with the component
async function createThing(formData: FormData) {
'use server'
const validated = schema.parse(Object.fromEntries(formData))
await db.thing.create({ data: validated })
revalidatePath('/things')
}
export function ThingForm() {
return <form action={createThing}>{/* ... */}</form>
}The mutation lives next to the UI that triggers it. No routing. No fetch.
When they shine
Progressive enhancement out of the box. The form works before JavaScript loads. This matters more than it sounds — on slow 4G connections in Indonesia, the page is often interactive before JS fully executes.
Type safety across the client-server boundary. The action function is typed. The form data still needs manual parsing, but zod handles that in two lines.
Revalidation is declarative. Call revalidatePath or revalidateTag and Next.js handles cache invalidation. No client-side refetch logic.
When they don't
I still use API routes for:
- Webhooks — Stripe, GitHub, anything that POSTs to your app from the outside
- Mobile clients — if a React Native app needs the same endpoint
- Complex response shapes — Server Actions return void or serializable values; sometimes you need a proper HTTP response with headers
The mental model
Think of Server Actions as RPC calls, not REST endpoints. They're not URLs. They're functions that happen to run on the server. Once you stop thinking "API" and start thinking "async function," the ergonomics click.
The tradeoff is coupling. Your server logic lives inside your UI code. For small projects and internal tools this is fine — even good. For large teams with separate backend/frontend ownership, you probably want explicit API boundaries.
For my projects: one developer, one repo, one deploy. Server Actions win every time.