What Are Server Actions?
Server Actions are one of the most developer-friendly features to land in Next.js in recent years. They let you define async functions that run exclusively on the server โ and call them directly from your components without ever writing an API route. In 2026, they’re stable, widely adopted, and genuinely change how you think about full-stack Next.js development.
The core idea: mark a function with "use server", and Next.js will ensure it only ever executes on the server โ even when invoked from a client-side event.
The Basics: Your First Server Action
Here’s a simple contact form handled entirely without an API route:
// app/contact/actions.ts
"use server";
export async function submitContact(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await saveToDatabase({ name, email });
}
// app/contact/page.tsx
import { submitContact } from "./actions";
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" />
<input name="email" type="email" />
<button type="submit">Send</button>
</form>
);
}
Notice there’s no onSubmit, no fetch, no JSON wrangling. The form just works โ and degrades gracefully without JavaScript.
Revalidating Data After Mutations
After a mutation, you’ll usually want to refresh the UI. Next.js makes this straightforward:
"use server";
import { revalidatePath } from "next/cache";
export async function addItem(formData: FormData) {
await db.insert({ name: formData.get("name") });
revalidatePath("/items");
}
revalidatePath tells Next.js to re-fetch and re-render the specified route, so users always see fresh data without a full page reload.
Using Server Actions with useActionState
For richer UX โ showing validation errors, loading indicators, or success messages โ combine Server Actions with the useActionState hook (formerly useFormState):
"use client";
import { useActionState } from "react";
import { submitContact } from "./actions";
export function ContactForm() {
const [state, action, isPending] = useActionState(submitContact, null);
return (
<form action={action}>
<input name="name" />
{state?.error && <p>{state.error}</p>}
<button disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</button>
</form>
);
}
Security Considerations
Server Actions are powerful, but they’re still HTTP endpoints under the hood. Always validate and sanitise input server-side โ never trust data passed from the client. Use a library like Zod to validate FormData before acting on it:
import { z } from "zod";
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export async function submitContact(formData: FormData) {
const parsed = schema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!parsed.success) return { error: "Invalid input" };
// proceed safely...
}
When Not to Use Server Actions
Server Actions shine for mutations triggered by user interaction. They’re not the right tool for complex REST APIs consumed by third parties, webhooks, or heavy background processing. For those cases, traditional Route Handlers (app/api/route.ts) are still the right call.
Wrapping Up
Server Actions are one of those features that feel almost too good to be true at first. Fewer files, less boilerplate, better progressive enhancement, and a cleaner mental model. Once you start using them, you’ll wonder how you ever lived without them.
Leave a Reply
You must be logged in to post a comment.