Command Palette

Search for a command to run...

Hectal
PHASE 20Advanced React 19+ ~28 min· topic 3 of 7

Topic 20.3

Server Functions and Server Actions

In one line

A Server Function is an async function marked "use server" that runs on the server but can be called from the client like a normal function; used as a form action it is called a Server Action, works before JavaScript loads, and is a public HTTP endpoint that must validate its input and authorise every call.

Think of it like this

The order slip at a bank counter. You fill in a slip (the form) and push it through the window (the network). The clerk (the server) does the work with the money you can't touch. But the clerk never trusts the slip: they check your ID, check that the account is yours and that the amount makes sense, every single time, because anyone can walk up and push a slip through the window.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Server Function
An async function marked "use server" that runs on the server but can be called from client code.
Server Action
A Server Function used for a form submission or other change (mutation).
"use server"
A directive that marks one function, or every export of a file, as a Server Function. It does not mean "server component".
Mutation
A change to stored data: create, update or delete.
Progressive enhancement
Building so the basic feature works without JavaScript and improves when JavaScript loads.
Authentication
Finding out who the user is, for example from a session cookie.
Authorisation
Checking that this user is allowed to do this action on this particular record.
Validation
Checking that input has the right type, length and range before using it.
CSRF
Cross-site request forgery: another website making a signed-in user's browser send a request to your site.

Step by step

01Write the Server Function in a "use server" file

Every export of this file becomes callable from the client, so each one starts with the same three checks: who is calling, may they do this, is the input valid.

app/actions.tswhole filets
"use server";
import { z } from "zod";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { updateTag } from "next/cache";

const PostInput = z.object({ title: z.string().trim().min(3).max(60) });

export async function createPost(prevState: { error: string | null }, formData: FormData) {
  const session = await auth();                       // 1. authenticate (cookie)
  if (!session?.user) return { error: "Please sign in." };
  if (session.user.role !== "author") return { error: "Not allowed." }; // 2. authorise

  const parsed = PostInput.safeParse({ title: formData.get("title") }); // 3. validate
  if (!parsed.success) return { error: "Title must be 3 to 60 characters." };

  await db.post.create({ data: { title: parsed.data.title, authorId: session.user.id } });
  updateTag("posts");                                 // refresh cached lists
  return { error: null };                             // 4. return only what the UI needs
}

02Call it from a form

A client component imports the function and hands it to useActionState. The form works like any React 19 action form (Phase 19), but the work happens on the server.

app/new-post-form.tsxwhole filetsx
"use client";
import { useActionState } from "react";
import { createPost } from "./actions";

export function NewPostForm() {
  const [state, formAction, pending] = useActionState(createPost, { error: null });
  return (
    <form action={formAction}>
      <label htmlFor="title">Title</label>
      <input id="title" name="title" required />
      <button disabled={pending}>{pending ? "Saving..." : "Publish"}</button>
      {state.error && <p aria-live="polite">{state.error}</p>}
    </form>
  );
}

03Or from an event handler

Outside forms, call the function like any async function. Wrap the call in startTransition (or use useTransition) so React treats it as an Action and you get a pending state.

app/like-button.tsxwhole filetsx
"use client";
import { useState, useTransition } from "react";
import { likePost } from "./actions";

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  const [pending, startTransition] = useTransition();
  return (
    <button
      disabled={pending}
      onClick={() => startTransition(async () => setLikes(await likePost(postId)))}
    >
      Like ({likes})
    </button>
  );
}

04What happens on the wire

The client never has the function's code, only its reference. The server rechecks everything because the request could have come from anywhere.

What happens on the wirediagram
Rendering diagram…

05Inline actions in server components

A server component can define an action inline. Variables it captures (postId here) are sent to the client encrypted and come back with the call, so they are tamper-resistant, but the action still re-checks the session.

app/posts/[id]/page.tsxwhole filetsx
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id: postId } = await params;

  async function archive() {
    "use server";
    const session = await auth();
    if (!session?.user || !(await canEdit(session.user, postId))) throw new Error("Forbidden");
    await db.post.update({ where: { id: postId }, data: { archived: true } });
  }

  return (
    <form action={archive}>
      <button>Archive</button>
    </form>
  );
}

Try it yourself

  1. 1

    Try the limits

    In the first example, type a 70-character title and press Publish. Predict the message. Then remove the length check from createPost and think: the <input> had no maxLength, but would adding maxLength={60} to the input make the server safe? (No: anyone can POST without your input.)

  2. 2

    Watch the form reset

    Publish a valid title by hand. Notice the input empties after the action finishes: React 19 resets uncontrolled forms after an action. Make the input controlled with state and compare.

  3. 3

    Break the authorisation yourself

    In the second example, change deleteNote to look the note up by n.id === noteId only. Predict the line for "someone else's note", run it, then put the owner check back.

Code & diagrams

A Server Action with validation and useActionState New tab

Simulated server: `createPost` would live in a `"use server"` file, and `serverReference` plays React's generated client stub (serialize the arguments, POST, deserialize the result). `useActionState`, the form `action` and the pending state are real React 19. Try your own titles in the box.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

screen: Title needs at least 3 characters.
server saved: Learning Server Functions
screen: Posts saved: 1
Server Functions are public: authorise every call New tab

A model of the defensive pattern from the Next.js security guide: take only an id from the client, get the user from the session, and look the record up **by id and owner** together. The unsafe version lets a signed-in user delete someone else's note just by changing the arguments.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

unsafe: notes left 1 of 2
safe, someone else's note: { ok: false, error: 'Not found' }
safe, no cookie: { ok: false, error: 'Unauthorized' }
safe, bad input: { ok: false, error: 'Invalid id' }
safe, own note: { ok: true }
left: [ 'Note by Ravi' ]
Progressive enhancement: a server component formtsx
// app/subscribe/page.tsx: a SERVER component, no "use client" anywhere
import { subscribe } from "./actions"; // "use server" file

export default function Page() {
  return (
    // React renders a real <form method="POST"> that targets the action.
    // Before hydration (or with JavaScript off) the browser submits it normally;
    // after hydration React submits it without a page reload.
    <form action={subscribe}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <button>Subscribe</button>
    </form>
  );
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Trust a userId sent by the client

Write export async function deleteNote(noteId: string, userId: string) and delete where { id: noteId, ownerId: userId }, passing userId from a client component.

terminal
$ What an attacker sees
── what you'll see ──
Any signed-in user can delete any note by calling the action with someone else's userId. The UI looks perfectly normal.

Break #2

Define a Server Function inside a client component

In a file with "use client", write async function save() { "use server"; ... } inside the component.

terminal
$ next dev (build error, abbreviated)
── what you'll see ──
It is not allowed to define inline "use server" annotated Server Actions in Client Components.
…

Myth vs fact

Myth

"use server" marks a Server Component.

Fact

Server components need no directive (they're the default in the App Router). "use server" marks Server Functions: functions the client may call. Putting it on a file makes every export a callable endpoint.

Myth

If the button is only shown to admins, the action is safe.

Fact

The action is a POST endpoint anyone can call. Check the user's role inside the function.

Myth

Server Actions are for fetching data.

Fact

They're designed for mutations. Next.js runs them one at a time per client. Read data in server components (or a data library) instead.

Myth

Framework CSRF protection means I don't need to validate.

Fact

CSRF checks stop other sites from forging requests through your users' browsers. They don't stop a user (or a script) sending bad data from their own browser. Validate and authorise anyway.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Treat a "use server" file as an API surface: export only functions meant to be called, keep helpers in a separate server-only module, and put checks in a data access layer (a module that every read and write goes through, with auth inside) so no action can forget them.

  • ▸

    Return values are serialized to the client exactly like props. Returning await db.user.findUnique(...) sends every column, including password hashes or internal flags. Map to a small DTO ({ id, name }). React's experimental taint APIs (experimental_taintObjectReference, experimental_taintUniqueValue) can make the serializer refuse a specific object or value as a second safety net.

  • ▸

    redirect() from next/navigation works by throwing a special error, so calling it inside a try/catch swallows it. Call it after the try block. Call updateTag/revalidatePath before redirect.

  • ▸

    Expected errors (validation, "not allowed") should be returned as state for useActionState, not thrown; thrown errors go to the nearest error boundary, and in production Next.js replaces server error messages with a generic one plus a digest id for your logs.

Remember this

  1. 1

    A Server Function is an async function that runs on the server and that client code can call. You mark it with the **"use server" directive: either as the first line inside the function body (allowed inside server components), or as the first line of a file, which makes every exported function in that file a Server Function. Client components can't define them, but they can import** them from a "use server" file or receive them as props from a server component.

  2. 2

    How a call works: the bundler replaces the function, in client code, with a reference (an encrypted action id plus a small dispatcher). When the client calls it, React serializes the arguments, sends an HTTP POST to the server, the framework finds the real function by its id, runs it, and serializes the return value back. Arguments and return values follow the serialization rules from Topic 20.2 (plus FormData). In Next.js the response can also carry updated server-component UI in the same round trip.

  3. 3

    A Server Function used to handle a form or a mutation is called a Server Action. React 19 lets you pass a function to <form action={fn}> or <button formAction={fn}>; the function receives a FormData. Hooks from Phase 19 work unchanged: useActionState(action, initialState) gives you the returned state and a pending flag, useFormStatus shows pending inside the form, and useOptimistic shows the change before the server confirms. After the action finishes, React resets uncontrolled form fields.

  4. 4

    Progressive enhancement means the page works at a basic level even before (or without) JavaScript, and gets better when JavaScript arrives. When a server component renders <form action={serverAction}>, React outputs a real HTML form that POSTs to the server, so a user on a slow connection can submit before hydration finishes; after hydration, React intercepts the submit and updates the page without a full reload. With useActionState you can pass a permalink (third argument) so the no-JavaScript submit lands on the right page.

  5. 5

    Security: every Server Function is a public endpoint. Anyone can send a POST to it with any arguments, not only your UI; hiding the button or checking the user on the page does not protect the function. Inside every Server Function: (1) authenticate: read the user from the session cookie on the server, never from an argument; (2) authorise: check this user may do this to this record (for example, filter by ownerId: session.userId); (3) validate every input's type, length and range (a schema library like Zod helps), because FormData values are untrusted strings; (4) return only what the UI needs, never whole database rows, tokens or internal errors. Phase 21.5 goes deeper.

  6. 6

    Frameworks add protections, but they are a second layer, not a replacement: Next.js compares the request's Origin with the Host header (a defence against CSRF, where another site makes your browser send a request), limits request bodies to 1 MB by default, uses encrypted, non-guessable action ids, removes unused Server Functions from the build, and encrypts variables captured by inline actions. Next.js runs Server Actions one at a time per client, so use them for mutations, not for parallel data fetching; read data in server components instead.

Explain it without notes

01

What is a Server Function, how do you declare one, and how does the client call it?

02

What is the difference between a Server Function and a Server Action?

03

What is progressive enhancement and how do Server Actions support it?

04

Why must every Server Function authorise and validate on its own? What checks belong in it?

Practice

01

Write a "use server" function renameList(listId: string, formData: FormData) that renames a to-do list only if the signed-in user owns it and the new name is 1 to 40 characters.

02

Write the client form for it using useActionState with a pending label and an error message.

Trade-offs

  • ↔

    Server Actions remove the need to write and call your own API routes for your own UI, but they're tied to the framework; if mobile apps or third parties need the same operation, a documented HTTP API (Route Handler) is still the right tool.

  • ↔

    Progressive enhancement makes forms robust on slow networks, but no-JavaScript submits reload the page, so design the action's result for both paths.

Done when you can

  • I can declare a Server Function with "use server" in a file or inline.

  • I can call one from a form, with useActionState, and from an event handler.

  • I can explain progressive enhancement for Server Actions.

  • I authenticate from the session, authorise per record and validate every input in every Server Function.

  • I return only the data the UI needs.