Server Functions and server actions

React · Expert · 16 min

Server Functions replace hand-written API routes for mutations, but each one is a network endpoint with security implications.

A Server Function is an async function marked with "use server" that runs on the server but can be called from the client. The framework's bundler replaces it in client code with a reference; calling it sends the arguments over the network, runs the function, and returns the serialised result.

Define them either inline in a Server Component (with "use server" at the top of the function body) or in a module that starts with "use server", whose exports all become Server Functions and can be imported by Client Components.

When a Server Function is passed to a form's action or called inside a transition, it's commonly called a server action. Combined with React 19's form features this is powerful:

<form action={createNote}>       // works even before JavaScript has loaded
  • useActionState(serverFn, initial) returns errors and results to the form; with a permalink argument, forms can submit progressively enhanced.
  • useFormStatus and useOptimistic work as with client Actions.
  • After a mutation, the framework revalidates or refreshes the affected Server Components (e.g. revalidatePath in Next.js).

Security is the main expert concern. Every Server Function is effectively a public HTTP endpoint: anyone can call it with any arguments, regardless of whether your UI shows the button. So inside each function:

  • Authenticate and authorise the caller (check the session, check ownership).
  • Validate all arguments (e.g. with Zod); FormData values are untrusted strings.
  • Return only what the client should see; closure variables captured by inline functions are encrypted by frameworks but shouldn't hold secrets.

Server Functions are designed for mutations; calls are processed sequentially, so they're not a good tool for fetching data. Load data in Server Components instead.

Example

// app/actions.js
"use server";
import { z } from "zod";
import { db } from "@/lib/db";
import { getSession } from "@/lib/auth";
import { revalidatePath } from "next/cache";

const NoteSchema = z.object({ title: z.string().min(1).max(100) });

export async function createNote(prevState, formData) {
  const session = await getSession();
  if (!session) return { error: "Please sign in." };          // authenticate

  const parsed = NoteSchema.safeParse({ title: formData.get("title") });
  if (!parsed.success) return { error: "Title must be 1-100 characters." };  // validate

  await db.notes.create({ ownerId: session.userId, title: parsed.data.title });
  revalidatePath("/notes");                                   // refresh server data
  return { error: null };
}

// NewNoteForm.jsx
// "use client";
// import { useActionState } from "react";
// import { createNote } from "./actions";
// export function NewNoteForm() {
//   const [state, action, pending] = useActionState(createNote, { error: null });
//   return <form action={action}><input name="title" /><button disabled={pending}>Add</button>
//     {state.error && <p>{state.error}</p>}</form>;
// }

Remember

  • "use server" makes an async function callable from the client over the network
  • Pass Server Functions to <form action> or useActionState for progressive enhancement
  • Treat each one as a public endpoint: authenticate, authorise and validate every call

Watch out for

  • Relying on the UI hiding a button for security; anyone can invoke the Server Function directly.

Check yourself

  1. A Server Function deleteNote(id) is only rendered for admins in the UI. Is it secure?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/reference/rsc/server-functions

Builds on

Unlocks

Nothing builds on this yet.