The Problem with API Routes
Before Server Actions, every mutation needed an API route, a fetch call, JSON serialization, and error handling on both sides. For a simple form submit, that's a lot of moving parts.
What Is a Server Action?
A Server Action is an async function that runs on the server but can be called from a client component as if it were a local function. You write it once, and Next.js handles the network boundary.
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.post.create({ data: { title } });
}Add "use server" at the top of the file (or the function), and it becomes a server-only function exposed to clients via a generated endpoint.
Using It in a Form
The simplest pattern: pass the action directly to a form.
import { createPost } from "./actions";
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}This works even without JavaScript — it's a progressive enhancement. With JS enabled, Next.js intercepts the submit and calls the action via fetch.
Calling from a Client Component
For interactive flows, wrap the call in useTransition so you get a pending state.
"use client";
import { useTransition } from "react";
import { deletePost } from "./actions";
export function DeleteButton({ id }: { id: number }) {
const [pending, startTransition] = useTransition();
return (
<button
onClick={() => startTransition(() => deletePost(id))}
disabled={pending}
>
{pending ? "Deleting..." : "Delete"}
</button>
);
}