Mutations and Server Actions
Server Actions make server mutations convenient, but convenience does not remove validation, authentication, authorisation, or error handling.
Treat every Server Action like an exposed server operation
A button being hidden does not protect the operation behind it. Validate the input and check the caller inside the action or inside the server-side domain function it invokes.
A reliable mutation sequence is boring on purpose: authenticate, authorise, validate, write, invalidate, then redirect or return a typed result.
Keep the write path obvious
'use server'
export async function updateProject(formData: FormData) {
const user = await requireUser()
const input = projectSchema.parse(Object.fromEntries(formData))
await requireProjectPermission(user, input.id)
await saveProject(input)
updateTag(`project:${input.id}`)
updateTag('projects')
redirect(`/projects/${input.slug}`)
}Validation errors are normal results
Bad form input is not an infrastructure failure. Return field-level information the UI can display near the relevant control instead of throwing everything into an error boundary.
Reserve thrown exceptions for failures the current flow cannot reasonably handle.
Forms are still useful without turning everything into client state
Server Actions work naturally with forms. Keep the server mutation independent from cosmetic client behaviour, then add pending state and optimistic UI only where they improve the experience.
Do not build a client-side state machine for a simple form just because the framework allows one.
Can you explain “Mutations and Server Actions” without reaching for the API reference?
Describe the main trade-off, one failure mode, and the decision you would make in a real project.