You built an API route, a fetch wrapper, a loading flag, and an error toast — all to save one form. React 19 deletes most of that. A Server Action is a function that runs on the server and wires straight into your form: no route, no fetch, no client glue.
The action
Mark a function with use server and it becomes callable from the client without an endpoint. Pass it to a form’s action prop and the browser posts to it directly. You read fields off FormData, do the work, and revalidate.
"use server";
export async function createTodo(formData: FormData) {
const title = String(formData.get("title") ?? "").trim();
if (!title) return { error: "Title is required" };
await db.todo.create({ data: { title } });
revalidatePath("/todos");
}That one file replaces a route handler, a client fetch, and the type you kept in sync between them. Delete all three.
State and pending
For feedback, wrap the action in useActionState. It hands you the latest return value, a wrapped action for the form, and an isPending flag — no useState, no manual try/catch.
Return an object with field-level errors and render them next to each input. One function does the mutation and the messaging.
For a plain submit button, useFormStatus is lighter still. It reads the parent form’s pending state, so you can disable the button and swap its label while the action runs.
Progressive enhancement
Because the action sits on the form’s action prop, the form works before any JavaScript loads. Submit early, on a slow phone, with JS disabled — the browser posts, the server runs, the page updates.
Once React hydrates, that same submit becomes an in-place update with no full reload. You wrote it once and got both behaviors free.
This matters more than it sounds. The slowest moment for any visitor is that first load, before the JavaScript is ready — and that’s exactly when a plain action form still works.
The rule
Client validation is a courtesy. The server is the wall. Validate every field there with something like Zod, and confirm auth before you touch the database.
Keep each action small — one job per function. Composition beats a 200-line mega-action you can’t test in isolation.
So: one function, use server, wired to action. Validate on the server, return errors as data, let the form degrade gracefully. That’s the whole pattern.