Before Server Actions, every mutation meant an API route, a fetch call, loading flags and error handling by hand. A Server Action is an async function that runs on the server and can be called directly from a form or a component; Next.js generates the endpoint for you. After this lesson you will be able to define actions, wire them to forms, refresh the UI after a write, and call actions from Client Components safely.
The "use server" directive marks a function, or every export of a file, as a Server Action:
// app/todos/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
export async function createTodo(formData: FormData) {
const title = String(formData.get("title") ?? "").trim();
if (!title) return;
await db.todo.create({ data: { title } });
revalidatePath("/todos");
}
export async function deleteTodo(id: string) {
await db.todo.delete({ where: { id } });
redirect("/todos");
}A dedicated actions file can be imported by both Server and Client Components, and the directive at the top applies to every export. An action can also be declared inline in a Server Component by placing "use server" as the first line of the function body.
Pass the action to the action attribute of a <form>, as in the sample at the top of this lesson. On submit the browser POSTs to the current URL; Next.js routes the request to the action with a FormData object and, because the action called revalidatePath, responds with the updated page in the same round trip. No onSubmit, no fetch, no manual state.
Because it is a plain HTML form, submission also works before JavaScript has loaded: progressive enhancement for free.
Actions accept ordinary parameters when called from code. With forms, add hidden inputs or bind arguments ahead of time:
// components/DeleteButton.tsx
import { deleteTodo } from "@/app/todos/actions";
export function DeleteButton({ id }: { id: string }) {
const deleteWithId = deleteTodo.bind(null, id);
return (
<form action={deleteWithId}>
<button type="submit">Delete</button>
</form>
);
}Bound arguments are encrypted before being embedded in the page, but you should still authorise the operation on the server.
A Client Component cannot define an action, but it can import one from a "use server" file and call it like any async function:
"use client";
import { useTransition } from "react";
import { toggleTodo } from "@/app/todos/actions";
export function TodoCheckbox({ id, done }: { id: string; done: boolean }) {
const [pending, startTransition] = useTransition();
return (
<input
type="checkbox"
checked={done}
disabled={pending}
onChange={() => startTransition(() => toggleTodo(id))}
/>
);
}useTransition provides a pending flag while the request is in flight.
Every exported action is a public POST endpoint, callable by anyone who discovers its id. Treat it like an API route:
FormData values are untrusted.Origin and Host headers to block cross-site submissions; set serverActions.allowedOrigins in next.config.ts when a proxy changes the host.redirect() in try/catch inside an action; it throws by design and the catch swallows it.revalidatePath or revalidateTag, so the form submits but the list on screen does not change.What does the `"use server"` directive at the top of a file do?
"use server", callable from forms and components.<form action={...}> to get progressive enhancement and automatic FormData handling.revalidatePath or revalidateTag after writes so the UI updates in the same round trip..bind or hidden inputs for extra arguments and useTransition in event handlers.Next lesson: Form State, Validation and Optimistic UI ā return validation errors, show pending states and update the UI before the server responds.