cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Server Actions

Write Server Actions with "use server", call them from forms and clicks, see why they run one at a time, and why every action is a public endpoint.

By the end, you can
  • 01
    Write a Server ActionMark a file or one async function in a Server Component with "use server", and know where the directive can't go.
  • 02
    Call one from a form or a clickPass it to a form that works before the JavaScript loads, or call it from a click inside a transition, with a pending state.
  • 03
    Read what it sendsSpot an action's POST and its next-action header, explain why actions run one at a time, and check every input inside it.

A function the browser can call

The tea shop's Mint tea page lists its reviews and has a box to write a new one. Saving a review means changing data on the server. In a client-rendered React app you would write an API address for it and fetch it from the browser. In Next.js you write a function, mark it "use server", and hand it to the form. The function stays on the server, and the page calls it anyway. Your first Next.js app saved a note this way; here is the same idea for reviews, recorded from real Next.js 16.4.3 and real Chrome.

A review form whose action runs on the serverNext.js
app/
app/actions.ts
"use server"; import { revalidatePath } from "next/cache";import { saveReview } from "./reviews"; export async function addReview(formData: FormData) {  const text = String(formData.get("text") ?? "").trim();  if (text) await saveReview(text);  revalidatePath("/");}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/
Mint tea
  • Cool and fresh.
  • Lovely with honey.
  • Smooth and calm.
Post review
Focus
the button “Post review”
Scrolled
0 px down
Announced to screen readers
Mint tea

Requests the page made during this step (1), without the files it loaded

  1. POST/200text/x-component
    next-router-state-tree
    %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C49314%5D%7D%2Cnull%2Cnull%2C49234%5D
    next-action
    40060689e296e335ce4040a5cf9094cdb10d60f823

Line 1 of app/actions.ts is "use server", a directive: a line of text that tells the build how to treat the code after it. Here it says that every function this file exports may be called from the browser. React calls such a function a Server Function, and one that a form or a transition calls to change data is a Server Action. Lines 6 to 10 are addReview. It gets the form's fields as formData, a FormData object. Line 7 reads the field named text, line 8 saves it, and line 9 tells Next.js that the stored HTML of / is out of date.

In app/page.tsx, line 14 hands the function to the form: <form action={addReview}>. The Steps tab is open on step 3, the click on Post review, after Chrome typed Smooth and calm. into the box. It made one request: a fetch, POST /, to the page's own address, with a next-action header. The answer is 200 with text/x-component. The review appeared in the list, the box is empty again, and the page never reloaded.

A browser window with a review box and a Post review button. An arrow labeled POST / with next-action goes to a server window where addReview(formData) runs. An arrow labeled the updated page comes back to the browser.browserSmooth and calm.Post reviewserveraddReview(formData)with next-actionPOST /the updated page
The form calls addReview as if it were in the browser. Next.js turns the call into a POST to the page, with the action's id in a next-action header. The function runs on the server, and the answer brings the updated page back in the same round trip.
Real-life analogyThe order button at a café counter

A café has a button on the counter for each drink. You press Mint tea; you never walk into the kitchen. The button sends an order number, the kitchen makes the tea, and the tea comes back with a receipt. The kitchen still checks every order it gets, because anyone can press the buttons.

In real life: The button on the counter labeled Mint tea
In Next.js: The form, or a button, that calls addReview
In real life: The kitchen, where the tea is really made
In Next.js: The server, where the action runs
In real life: The order number the button sends to the kitchen
In Next.js: The action's id, in the next-action header
In real life: The tea and the receipt, handed back together
In Next.js: The updated page, in one answer
The main idea

A Server Action is an async function marked "use server" that runs on the server when the page calls it. A form calls it with its fields, and a click calls it inside a transition. Next.js turns each call into a POST request to the page, and the answer can bring back the updated page in the same round trip.

Writing a Server Action

"use server" goes in one of two places. At the very top of a file, before the imports, it makes every exported function an action, as in app/actions.ts. Or at the top of one async function's body, inside a Server Component: an inline action, which only that component can hand out. Try both, and two places where it doesn't work. Each button is one version of the review page, built with real Next.js. Before you click, guess which ones build.

Playground: where can "use server" go?
The Mint tea page with "use server" in this placeNext.js
app/
app/actions.ts
"use server"; import { revalidatePath } from "next/cache";import { saveReview } from "./reviews"; export async function addReview(formData: FormData) {  const text = String(formData.get("text") ?? "").trim();  if (text) await saveReview(text);  revalidatePath("/");}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (3/3)  Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○  (Static)  prerendered as static content
Try it yourself
At the top of a file

Line 1 marks every export of app/actions.ts as a Server Action. The page imports addReview, the build passes, and the Network tab's POST / saved a review with no JavaScript.

Every version was built with next build from Next.js 16.4.3. The Terminal shows what the build printed; for the two that built, the Network tab shows the form posted with no JavaScript. Reset goes back to the first version.

Two build and two don't:

  • At the top of a file is the usual place. Server Components and Client Components can both import the actions, and one file can hold all of a feature's actions.
  • Inside a Server Component, line 8 of app/page.tsx starts the body of addReview. An inline action can use the component's variables, like a tea's id; Next.js's docs say it encrypts the ones the action uses before they travel to the browser and back.
  • Inside a Client Component, the build stops: It is not allowed to define inline "use server" annotated Server Actions in Client Components. The error names the two fixes: import the action from a file that starts with "use server", or pass it down as a prop from a Server Component, as Composing Server and Client Components did with addAction.
  • On a function that isn't async, the build stops too: Server Actions must be async functions. A call from the browser is a network request, which always takes time, so the function has to return a promise.

Each row is one place for the directive, what it marks, and who can call what it marks:

Where "use server" can go
Where you write itWhat it marksWho can use it
At the top of a file, before the importsEvery function the file exports is a Server Action.Any component: Server Components and Client Components can import them.
At the top of an async function's body, in a Server ComponentThat one function is a Server Action.That Server Component, and the components it passes the function to as a prop.
At the top of a function in a Client ComponentNothing: the build stops with an error.Nobody. Import the action from a "use server" file instead.
On a function that isn't asyncNothing: the build stops with an error.Nobody. A call is a network request, so the function must be async.
"use server" is not the opposite of "use client"

Components are Server Components unless a "use client" file says otherwise, so no directive makes a Server Component. "use client" marks where the browser's part of the app starts. "use server" marks functions that stay on the server but that the browser may call. Arguments and return values travel over the network, so they must be serializable; react.dev lists the types that can cross.

Calling one from a form

<form action={addReview}> is React's way to connect a form to a function (Form actions). When the visitor submits, React calls the function with the form's fields, in a transition. With a Server Action, the form also works before the page's JavaScript has loaded, or on a phone where it never does. Building a page so that it works as plain HTML first, and better once its JavaScript runs, is called progressive enhancement.

The recording shows both ways. Open the Network tab of the example above and pick /: the HTML's form says method="POST", with a hidden field named $ACTION_ID_ and a long code, which names the action. Then pick POST /, press Post review, before JavaScript: the recorder sent the form with Lovely with honey. the way a browser does with no JavaScript at all. Your first Next.js app's action ended with redirect() and got a 303. This one doesn't redirect, so the answer is the page itself: 200, a whole HTML page that already lists Lovely with honey., which the browser loads. The next request, / after posting, says x-nextjs-cache: MISS: line 9's revalidatePath made Next.js render the page again.

Each row compares the same press of Post review, before and after the JavaScript has loaded:

The same form, two ways
QuestionBefore the JavaScript loadsAfter it loads
What does the browser send?The form itself: POST /, with the text and a hidden field that names the action.A fetch: POST /, with a next-action header that names the action.
What comes back?A whole new HTML page, 200, with the new review in it.An RSC payload, 200 text/x-component, with the updated page.
What does the visitor see?The browser loads the new page.The review appears in place. No page load.
Two rows. Top, before the JavaScript loads: the form sends a POST with a hidden field, and a whole HTML page comes back, which the browser loads. Bottom, after it loads: a fetch sends a POST with a next-action header, and an RSC payload comes back, which the router puts on the page in place.Before the JavaScript loadsthe formPOST + hidden fielda whole HTML pagethe browser loads itAfter it loadsfetchPOST + next-actionRSC payloadthe router updates the page
The same form, two ways. Before the JavaScript loads, the browser posts the form itself and loads the HTML page that comes back. After, React sends a fetch with a next-action header, and the router puts the RSC payload on the page in place. Either way, addReview runs once, on the server.

One more thing changed with JavaScript: the box is empty after step 3. After a form's action finishes without an error, React resets the form's uncontrolled fields, the ones whose value you don't keep in state yourself. A field you control with value and onChange keeps what's in your state. To show errors next to the box and keep what the visitor typed, the Forms lesson in this stage uses useActionState.

Calling one from a click

Not every change has a form. An Add to cart button sends only the tea's id. A Client Component can import an action from a "use server" file and call it from an event handler, like any async function:

An Add button that calls a Server Action in a transitionNext.js
app/
app/add-button.tsx
"use client"; import { useTransition } from "react";import { addToCart } from "./actions"; export default function AddButton({ id, name }: { id: string; name: string }) {  const [isPending, startTransition] = useTransition();   function add() {    startTransition(async () => {      await addToCart(id);    });  }   return <button onClick={add}>{isPending ? `Adding ${name}…` : `Add ${name}`}</button>;}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Tea shop
Adding Mint tea…Add Green tea

In your cart: nothing yet

Focus
the button “Adding Mint tea…”
Scrolled
0 px down

Requests the page made during this step (1), without the files it loaded

  1. POST/no answer yetfetch
    next-router-state-tree
    %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C514%5D%7D%2Cnull%2Cnull%2C530%5D
    next-action
    40a9d7622a64e96fd85cd0272e1d38cd68e41c70f8

In app/add-button.tsx, line 4 imports addToCart from app/actions.ts. Line 7 calls useTransition, which gives back isPending and startTransition. Lines 9 to 13 are the click handler: it calls addToCart(id) inside startTransition. react.dev asks for this outside forms: a form puts the call in a transition for you, and a button doesn't. While the transition runs, isPending is true, and line 15 shows Adding Mint tea….

In app/actions.ts, addToCart prints a line, waits for addTea from app/cart.ts, prints another, and calls revalidatePath("/"). The stand-in database in app/cart.ts takes three seconds, so the wait is easy to see. The Steps tab is open on step 2, the click: the button says Adding Mint tea…, and Chrome sent one POST / with a next-action header that had no answer yet when the step ended. Step 3 waited for the cart: In your cart: Mint tea, and no new request. The answer to step 2's POST carried the updated page, because the action called revalidatePath. Next.js's docs call this a single round trip: the action's result and the new page in one response. The Terminal tab shows what next start printed: addToCart(mint) started, then addToCart(mint) finished.

The argument, "mint", travels in the request's body. Arguments and return values must be serializable, and only the id crosses here; addTea looks the tea up on the server. The docs also show an action called from an Effect, like counting a view once a page opens, inside startTransition as well.

Actions run one at a time

What if the visitor clicks Add Mint tea and then, before it's done, Add Green tea? The recorder did exactly that in real Chrome. Before you open the Steps tab, guess: does Chrome send the second POST right away?

Two quick clicks on two Add buttonsNext.js
app/
app/actions.ts
"use server"; import { revalidatePath } from "next/cache";import { addTea } from "./cart"; export async function addToCart(id: string) {  console.log(`addToCart(${id}) started`);  await addTea(id);  console.log(`addToCart(${id}) finished`);  revalidatePath("/");}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Tea shop
Adding Mint tea…Adding Green tea…

In your cart: nothing yet

Focus
the button “Adding Green tea…”
Scrolled
0 px down

No requests during this step: the page already had what it needed

Step 2 clicked Add Mint tea: one POST /, with no answer yet. Step 3, open now, clicked Add Green tea while the first was still running. Both buttons say Adding…, yet step 3 lists no request at all. Step 4 waited for both teas, and lists one POST /: the green one, sent after the mint one had answered. The Terminal tab agrees, line by line: addToCart(mint) started, addToCart(mint) finished, and only then addToCart(green) started and addToCart(green) finished.

A timeline from 0 to 6 seconds. The first row, addToCart mint, runs from 0 to 3 seconds. The second click comes while it still runs; a card shows it waiting until 3 seconds. Then addToCart green runs from 3 to 6 seconds.mintaddToCart("mint")greenwaitingaddToCart("green")second click0 s3 s6 s
The second click comes while addToCart("mint") is still running. Next.js holds it in line, so Chrome sends the second POST only after the first has answered. The stand-in database takes three seconds, so the whole wait is easy to see.

That's by design. Next.js's docs say it dispatches Server Actions one at a time for each client: the second waits for the first to finish, so the page the router shows always matches the action that produced it. Three things follow:

  • The server doesn't queue them. The line is in the browser's copy of the router. Each action is its own request, and the server runs whatever arrives, so two browser tabs, or two visitors, can run actions at the same time.
  • Promise.all doesn't make them parallel. Calling two actions at once from the same page still sends them one after the other. For work that must happen together, do it inside one action.
  • Actions are for changes, not for reading. react.dev says Server Functions are designed for changes, not for loading data, and that frameworks run them one at a time and don't cache what they return. Read in Server Components, or from a Route Handler in the browser, as Fetching in the browser does.
Read it in Next.js's own code

In release v16.4.3, callServer in sequential-call-server.ts hands every action to the router's queue. dispatchAction, at lines 186 to 226 of app-router-instance.ts, starts it at once when the queue is empty and otherwise adds it to the end, to be started after the one before it finishes. The file's comments name an experimental option that swaps in a different queue; the docs describe the one-at-a-time default. The Under the hood stage reads this code.

What an action's request looks like

Every recording so far shows the same kind of request. Here are its parts, as far as the Steps and Network tabs show them. Each row is one part, with the JavaScript loaded and without it:

An action's request
PartWith JavaScriptWithout JavaScript
The method and addressPOST to the page's own address, /.POST to the page's own address, /.
Which action to runThe next-action header.A hidden field, $ACTION_ID_ followed by the id.
The argumentsIn the request's body.The form's fields, in the request's body.
The answertext/x-component: an RSC payload for the router.text/html: a page for the browser to show.
  • It's a POST to the page you're on. There's no address of its own. The docs say only POST can call an action, and the recordings never show another method.
  • The next-action header names the action with an id the build gave it. Both clicks in the last example sent the same id, because they called the same function; the tea's id was in the body. In the review recording, the form's hidden field and the header carry the same id.
  • The answer is an RSC payload, text/x-component: the action's return value and, when the action changed what the page shows, the page rendered again. The request also carries a next-router-state-tree header, the router's description of what's on the screen, which the server uses when it renders the page again.

How the body and the payload are written, and how the build makes the ids, belong to the Under the hood stage. One more answer is possible: when an action calls redirect(), the payload holds the new page and the router moves there, with status 200, as Redirects in Next.js recorded.

Every action is a public endpoint

An endpoint is an address on a server that anyone on the internet can send a request to. A Server Action is one, even though you never wrote an address. The recorder proves it: its POST / before JavaScript wasn't sent by a browser at all, only by a small program that copied the form's fields. Any program can do the same with any action whose id it has, and send whatever arguments it likes.

Three senders on the left: the review form, an Add button, and any program that sends HTTP. Arrows from all three meet at the same request: POST with next-action. An arrow goes on to addToCart(id) on the server, labeled: checks every input.the review forman Add buttonany other programPOST + next-actionaddToCart(id)checks every input
Your form and your button aren't the only ways in. Any program that sends the same POST, with the action's id, runs the action too. So the action itself checks who's asking and what they sent.

So the action itself is where the checks go. Next.js's docs list them:

  • Check who's asking, and whether they may. Showing the form only to signed-in staff protects nothing, because the request can come without the form.
  • Check every argument. formData and every other argument come from the visitor. Line 10 of app/cart.ts refuses an id that isn't a tea, because the button isn't the only way to send one.
  • Return only what the page needs. Whatever an action returns travels to the browser.

Next.js adds a few protections of its own, which the docs describe: it compares the request's Origin header with the site's address and refuses actions sent from other sites' pages, it limits a request's body to 1 MB unless you change it, it makes action ids hard to guess and leaves out the ones no page uses, and it encrypts the variables an inline action captures. None of them knows who your visitor is. Securing Server Actions calls an action from outside the page and goes through each of them.

Planning the tea shop's actions

Each change in the shop needs a way to call its action. Forms suit anything with fields, and they work before the JavaScript loads. Buttons without fields call the action from a click, in a transition. And some things aren't changes at all. Sort these jobs:

How should this job reach the server?
  • Post a review with a text box and a button.
  • Add a tea to the cart from a button next to it.
  • Show the tea's reviews on its page.
  • Sign up for the newsletter with an email box.
  • Count a view once the tea's page has opened.
  • Show teas that match what the visitor is typing.
Try it yourself
0 of 6 correct

Sort each job: a form's action, an action called from a click or an Effect, or not an action at all.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The third group is the one people miss. An action can return data, but actions run one at a time and aren't meant for reading, so data that only needs to be shown comes from a Server Component, or from a Route Handler when the browser has to ask.

Common mix-ups

  • ""use server" makes a Server Component." No. Components are Server Components without any directive. "use server" marks functions the browser may call.
  • "A Server Action has its own API address." No. It's a POST to the page it's called from, with a next-action header.
  • "Only my form can call my action." No. Anyone can send the same POST. Check who's asking and what they sent inside the action.
  • "A form with an action needs JavaScript." No. The recorder posted the review form with none, and the review was saved.
  • "Two quick clicks run two actions at once." No. Next.js sends the second after the first has answered, even with Promise.all.
  • "Server Actions are a good way to load data." No. They're for changes. Read data in Server Components or from Route Handlers.
  • "A Client Component can write its own action." No. It imports one from a "use server" file, or gets one as a prop.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upRead an action's request

The review form calls this addReview. Which HTTP method does pressing Post review use?

Starter codeTypeScript
"use server";

import { revalidatePath } from "next/cache";
import { saveReview } from "./reviews";

export async function addReview(formData: FormData) {
  const text = String(formData.get("text") ?? "").trim();
  if (text) await saveReview(text);
  revalidatePath("/");
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPost before the JavaScript arrives

    A visitor on a slow train opens the Mint tea page. The page's JavaScript is still loading when they type a review and press Post review. Is the review saved?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix a Client Component's action

      This review form is a Client Component, and the build stops at line 7. Where should addReview live instead?

      Starter codeTSX
      "use client";
      
      import { saveReview } from "./reviews";
      
      export default function ReviewForm() {
        async function addReview(formData: FormData) {
          "use server";
          await saveReview(String(formData.get("text") ?? ""));
        }
      
        return (
          <form action={addReview}>
            <input name="text" aria-label="Review" />
            <button>Post review</button>
          </form>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeShow that the click is working

        This button saves the tea, but for three seconds nothing on the page changes, and visitors press it again. Which React hook lets it say Adding Mint tea… while the action runs?

        Starter codeTSX
        "use client";
        
        import { addToCart } from "./actions";
        
        export default function AddButton({ id, name }: { id: string; name: string }) {
          return <button onClick={() => addToCart(id)}>Add {name}</button>;
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeTwo actions with Promise.all

          Add both calls addToCart twice at once, with Promise.all. Each action prints when it starts and finishes. Which line does the Terminal print right after addToCart(mint) started?

          Starter codeTSX
          "use client";
          
          import { useTransition } from "react";
          import { addToCart } from "./actions";
          
          export default function AddBoth() {
            const [isPending, startTransition] = useTransition();
          
            function addBoth() {
              startTransition(async () => {
                await Promise.all([addToCart("mint"), addToCart("green")]);
              });
            }
          
            return <button onClick={addBoth}>{isPending ? "Adding both…" : "Add both"}</button>;
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFind why the build stops

            With this app/actions.ts, next build stops. Which word is missing?

            Starter codeTypeScript
            "use server";
            
            import { revalidatePath } from "next/cache";
            import { saveReview } from "./reviews";
            
            export function addReview(formData: FormData) {
              const text = String(formData.get("text") ?? "").trim();
              if (text) saveReview(text);
              revalidatePath("/");
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask where the function runs, and what travels between the browser and the server.

              The Server Actions quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does "use server" at the top of app/actions.ts do?

                Choose an answer to see the explanation.

              2. Question 2 of 7Before the page's JavaScript loads, a visitor posts a review. addReview doesn't redirect. What comes back?

                Choose an answer to see the explanation.

              3. Question 3 of 7With the JavaScript loaded, what does pressing Post review send?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why does this button call addToCart inside startTransition?

                Read the code, then predictTSX
                "use client";
                
                import { useTransition } from "react";
                import { addToCart } from "./actions";
                
                export default function AddButton({ id, name }: { id: string; name: string }) {
                  const [isPending, startTransition] = useTransition();
                
                  function add() {
                    startTransition(async () => {
                      await addToCart(id);
                    });
                  }
                
                  return <button onClick={add}>{isPending ? `Adding ${name}…` : `Add ${name}`}</button>;
                }
                

                Choose an answer to see the explanation.

              5. Question 5 of 7A visitor clicks Add Mint tea, then Add Green tea while the first is still running. When does Chrome send the second POST?

                Choose an answer to see the explanation.

              6. Question 6 of 7Where can "use server" sit inside a function?

                Choose an answer to see the explanation.

              7. Question 7 of 7The shop shows its Delete review form only to signed-in staff. Does deleteReview still need to check who's asking?

                Choose an answer to see the explanation.

              Key takeaways

              • "use server" at the top of a file makes every export a Server Action; at the top of an async function in a Server Component, it makes that one. Client Components import actions or get them as props.
              • A form calls an action with its fields, and works before the JavaScript loads: the browser posts the form and gets a page, or follows a 303 when the action redirects.
              • With JavaScript, a call is a fetch: POST to the page, with a next-action header, answered with an RSC payload that can hold the updated page.
              • Outside forms, call actions in startTransition, and show isPending while they run.
              • Next.js sends a page's actions one at a time; the server doesn't queue them. Use actions for changes, not for reading.
              • Every action is a public endpoint. Check who's asking and every argument inside it.

              Remember the one-liner.
              A Server Action is a function the browser calls and the server runs: one POST at a time, open to anyone.

              Coming next: Forms. It validates what the visitor typed on the server, shows errors next to the fields, and keeps the text with useActionState.

              CompleteFrontend Clear concepts. Working examples.