Shipping a real feature with only server actions
July 18, 2026
One month rewriting our booking flow to lean entirely on server actions — what stayed, what broke, and the four rules I would keep for next time.
What I actually changed
We took our booking flow — a five-step wizard with three separate mutation endpoints — and moved every write onto a server action. That means no more /api/booking/create, no more client-side POST helpers, no more zod-parsing-on-the-edge. Just use server at the top of the file and <form action={submit}>.
The good
Half the code disappeared. Optimistic UI got easier because useActionState gives you exactly the two things you want: the last returned value and the pending flag. And typed error returns beat throwing.
The bad
Streaming validation is harder. Server actions want a full round trip. For inline field validation we still hit a tiny /api route with a debounce.
Four rules
- One action per form. Do not compose actions inside actions — you will regret it the first time you need to redirect.
- Return, don't throw.
{ ok: false, error: '...' }is the shape. - Revalidate paths at the boundary. Never inside the action helper.
- Move the schema next to the action. Reuse it in the client only for optimistic guards.