Server Actions are functions that run on the server but can be called from a Client Component or directly from a <form action={…}>. They replace 90% of the boilerplate of API routes for write operations.
1"use server"; // marks every export in this file as a Server Action
2
3import { db } from "@/lib/db";
4
5export async function createCourse(formData: FormData) {
6 const title = formData.get("title")?.toString() ?? "";
7 if (!title) throw new Error("Title is required");
8 return db.course.create({ data: { title, slug: slugify(title) } });
9}1import { createCourse } from "./actions";
2
3export default function NewCourse() {
4 return (
5 <form action={createCourse}>
6 <input name="title" required />
7 <button>Create</button>
8 </form>
9 );
10}This is a Server Component with a server-side form action. It works without JavaScript — progressive enhancement by default.
1"use client";
2import { createCourse } from "./actions";
3
4export function NewCourseButton() {
5 return (
6 <button onClick={async () => {
7 const fd = new FormData();
8 fd.append("title", "Untitled course");
9 await createCourse(fd);
10 }}>+ New course</button>
11 );
12}Or with typed input (no FormData):
1// actions.ts
2"use server";
3
4export async function createCourseTyped(input: { title: string }) {
5 return db.course.create({ data: { title: input.title } });
6}1// Client
2const course = await createCourseTyped({ title: "New course" });| Location | Note |
|---|---|
In a actions.ts file with "use server"; at the top | Every export is an action |
| Inline inside a Server Component body | Mark the function itself with "use server" |
1export default function Page() {
2 async function save(formData: FormData) {
3 "use server";
4 // …
5 }
6 return <form action={save}>{/* … */}</form>;
7}Use file-level actions for anything reusable. Use inline actions for one-off forms close to where they're used.
1"use server";
2
3export async function rename(id: string, name: string) {
4 const course = await db.course.update({ where: { id }, data: { name } });
5 return { ok: true, course };
6}The return value is a normal value to the caller — typed end-to-end.
1"use server";
2
3export async function deleteCourse(id: string) {
4 try {
5 await db.course.delete({ where: { id } });
6 return { ok: true };
7 } catch (e) {
8 return { ok: false, error: "Cannot delete a published course" };
9 }
10}Prefer typed error return values over throwing. Form handlers cope with them gracefully via useActionState.
Server Actions are POSTed as a multipart form (when called from a form) or as an RPC-style request (when called directly). Next.js generates a stable, signed action ID — there is no public API route you accidentally expose. The function body never reaches the browser.