Mutating Data with Server Actions
Server Actions are async functions that run on the server. Forms can call them with action={fn}. You get progressive enhancement: the form still works if JS is slow or disabled.
"use server" — mutations that stay on the server
Server Actions are async functions that run on the server. Forms can call them with action={fn}. You get progressive enhancement: the form still works if JS is slow or disabled.
Real-life example: A Server Action is handing a sealed envelope to the receptionist — the guest never walks into the vault to update the ledger.
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
import { sql } from "@vercel/postgres";
import { redirect } from "next/navigation";
export async function createInvoice(formData: FormData) {
const customerName = String(formData.get("customerName") ?? "").trim();
const amount = Number(formData.get("amount"));
if (!customerName || Number.isNaN(amount) || amount <= 0) {
throw new Error("Invalid invoice data");
}
await sql`
INSERT INTO invoices (customer_name, amount, status)
VALUES (${customerName}, ${amount}, 'pending')
`;
revalidatePath("/invoices");
revalidateTag("invoices");
redirect("/invoices");
}Forms with action=
Wire the action directly on the form. Prefer name attributes so FormData picks up fields automatically.
import { createInvoice } from "../actions";
export default function NewInvoicePage() {
return (
<form action={createInvoice}>
<label htmlFor="customerName">Customer</label>
<input id="customerName" name="customerName" required />
<label htmlFor="amount">Amount (₹)</label>
<input id="amount" name="amount" type="number" min={1} required />
<button type="submit">Create invoice</button>
</form>
);
}revalidatePath and revalidateTag
After a write, cached reads are stale. revalidatePath('/invoices') refreshes that route. revalidateTag('invoices') refreshes every fetch tagged with invoices.
Real-life example: Revalidation is telling the library to reprint the catalog after you add a new book — otherwise readers keep seeing yesterday's list.
- revalidatePath — good for one page or layout
- revalidateTag — good when many pages share the same tagged fetch
- redirect after success so the browser lands on a fresh list