Server Actions are Next.js's answer to form handling and mutations without writing separate API routes. They let you call server-side code directly from client components — the function runs on the server, but you call it like a normal async function.
Here's how they work, when to use them, and the security considerations you need to understand before using them in production.
What Server Actions are
A Server Action is an async function marked with "use server" that runs on the server even when called from a client component. Under the hood, Next.js creates an HTTP endpoint for each action and handles the serialization automatically.
// app/actions/projects.ts
"use server";
import { auth } from "@clerk/nextjs/server";
import { revalidatePath } from "next/cache";
import { adminClient } from "@/lib/supabase/admin";
import { getUserDbId } from "@/lib/db";
export async function createProject(name: string) {
const { userId } = await auth();
if (!userId) throw new Error("Unauthorized");
const dbUserId = await getUserDbId(userId);
if (!dbUserId) throw new Error("User not found");
const { data, error } = await adminClient
.from("projects")
.insert({ name, user_id: dbUserId })
.select()
.single();
if (error) throw new Error(error.message);
revalidatePath("/dashboard");
return data;
}
Calling from a client component
Import and call the action directly — no fetch, no API route, no JSON serialization to manage:
"use client";
import { useState } from "react";
import { createProject } from "@/app/actions/projects";
export function CreateProjectForm() {
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
try {
await createProject(name);
setName("");
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Project name"
className="rounded border px-3 py-2 text-sm"
/>
<button type="submit" disabled={loading} className="rounded bg-zinc-900 px-4 py-2 text-sm text-white">
{loading ? "Creating…" : "Create"}
</button>
{error && <p className="text-sm text-red-500">{error}</p>}
</form>
);
}
Using with the native form action
Server Actions can also be passed directly to form action props, which works even without JavaScript (progressive enhancement):
// Server component — no "use client" needed
import { createProject } from "@/app/actions/projects";
export function CreateProjectForm() {
async function handleAction(formData: FormData) {
"use server";
const name = formData.get("name") as string;
await createProject(name);
}
return (
<form action={handleAction}>
<input name="name" placeholder="Project name" />
<button type="submit">Create</button>
</form>
);
}
This pattern is useful for simple forms in server components. For complex interactive forms with validation feedback, the client component approach gives more control.
useFormState for validation feedback
React 19 (which Next.js 15+ uses) provides useActionState (previously useFormState) for progressive form state:
"use client";
import { useActionState } from "react";
import { createProject } from "@/app/actions/projects";
type State = { error?: string; success?: boolean };
async function createProjectAction(
prevState: State,
formData: FormData
): Promise<State> {
const name = formData.get("name") as string;
if (!name?.trim()) return { error: "Name is required" };
try {
await createProject(name);
return { success: true };
} catch (err) {
return { error: err instanceof Error ? err.message : "Failed" };
}
}
export function CreateProjectForm() {
const [state, formAction, isPending] = useActionState(createProjectAction, {});
return (
<form action={formAction}>
<input name="name" placeholder="Project name" />
<button type="submit" disabled={isPending}>
{isPending ? "Creating…" : "Create"}
</button>
{state.error && <p className="text-red-500">{state.error}</p>}
{state.success && <p className="text-green-500">Project created!</p>}
</form>
);
}
Security: treat Server Actions like API routes
Server Actions are just API endpoints with a different calling convention. The security requirements are identical to route handlers:
- Always authenticate. The
"use server" directive does not protect an action. Anyone who can call your action URL (which Next.js creates automatically) can run it. - Validate inputs. Use Zod to validate the arguments before touching the database.
- Rate limit sensitive actions. Create-account, delete-account, and payment-related actions should have rate limits.
"use server";
import { auth } from "@clerk/nextjs/server";
import { z } from "zod";
const schema = z.object({
name: z.string().min(1).max(100),
});
export async function createProject(rawName: string) {
// 1. Authenticate
const { userId } = await auth();
if (!userId) throw new Error("Unauthorized");
// 2. Validate
const { name } = schema.parse({ name: rawName });
// 3. Do the work
// ...
}
When to use Server Actions vs API routes
Use Server Actions for:
- Form submissions in your own UI
- Mutations triggered by user interactions (create, update, delete)
- Operations that need to revalidate Next.js cache after running
Use API routes for:
- Webhooks (Stripe, Clerk) — external services call these, not your UI
- Public APIs consumed by third-party clients
- Streaming responses (Server-Sent Events)
- Operations that need custom response headers or status codes
revalidatePath and revalidateTag
After a mutation, call revalidatePath to clear the Next.js cache for a page, or revalidateTag to clear tagged cache entries:
import { revalidatePath, revalidateTag } from "next/cache";
// After creating/updating/deleting a project
revalidatePath("/dashboard"); // re-fetch the dashboard page data
revalidatePath(`/projects/${id}`); // re-fetch a specific project page
// Or with tags (requires tagging fetch calls)
revalidateTag("projects"); // invalidate all cached data tagged "projects"