Server Actions
Running server-side mutations directly from a component with the use server directive, no separate API route required.
3 min de lectura
Before Server Actions, mutating data from a Next.js app meant building an API route, then calling it with fetch from the client — two files and a network request just to save a form. A Server Action collapses that into one function you call almost like a normal one, while its actual execution stays entirely on the server.
Defining a Server Action
Mark an async function with "use server" — either inline inside a Server Component, or at the top of a dedicated file to mark every export in it:
// app/lib/actions.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title");
const content = formData.get("content");
await db.insert(posts).values({ title, content });
revalidatePath("/posts");
}Anything with "use server" becomes a Server Function — callable from the client, but its body only ever runs on the server. Under the hood, invoking it is a POST request Next.js manages for you; you never write the fetch call yourself.
Wiring it to a form
React extends the native <form> element so its action prop can point directly at a Server Function. Submitting the form calls the action and passes it a FormData object built from the form's inputs automatically:
import { createPost } from "@/app/lib/actions";
export default function NewPostForm() {
return (
<form action={createPost}>
<input type="text" name="title" placeholder="Title" />
<textarea name="content" placeholder="Write something..." />
<button type="submit">Publish</button>
</form>
);
}This form works before any client JavaScript has loaded — Server Components support progressive enhancement by default, so a slow connection or a JS-disabled browser still submits a real HTML form to a real server endpoint.
Security: never trust the caller
Because a Server Action is reachable as a direct POST request — not just through your UI — it needs to verify authentication and authorization itself, every time, exactly like an API route would:
"use server";
import { auth } from "@/lib/auth";
export async function deletePost(id: string) {
const session = await auth();
if (!session?.user) {
throw new Error("Unauthorized");
}
// Also verify the user actually owns this post before deleting it
await db.delete(posts).where(eq(posts.id, id));
}Skipping this check because "the button is only shown to logged-in users" is a real vulnerability — the button being hidden doesn't stop someone from calling the action directly.
Refreshing and redirecting after a mutation
A Server Action typically needs to do one of two things once the mutation succeeds: tell Next.js to refetch stale data, or send the user somewhere new.
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const post = await db.insert(posts).values({
title: formData.get("title"),
});
revalidatePath("/posts");
redirect(`/posts/${post.id}`);
}redirect() works by throwing a special exception Next.js catches internally — so any code written after it in the function never runs. If you need both a revalidation and a redirect, call revalidatePath (or revalidateTag) first.
Server Actions are the App Router's answer to "how do I mutate data" — the next lesson builds on this to handle validation errors and pending UI state in forms properly.