R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 26 of 42Article18 min

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.

app/invoices/actions.ts
"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.

app/invoices/new/page.tsx
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
Keep validation and auth checks inside the Server Action. Never trust the client alone — anyone can POST form fields.