---
title: "Next.js: Server Actions"
slug: "nextjs-server-actions"
category: "frontend"
tags: ["frontend", "nextjs", "server-actions", "mutations", "forms"]
status: "stable"
last_updated: 2026-08-14
summary: "Server Actions in Next.js 16: the use server directive, FormData inputs, progressive enhancement, revalidate/updateTag, and input validation."
related: ["[[frontend/nextjs]]", "[[frontend/nextjs-app-router]]", "[[frontend/nextjs-caching]]", "[[frontend/nextjs-route-handlers]]", "[[frontend/react-forms]]", "[[frontend/react-server-components]]", "[[backend/postgres]]"]
---

## Overview

Server Actions are async functions that run on the server and are callable from the client through an RPC reference the framework injects at build time. They are the default tool for mutations in Next.js 16: form submissions, optimistic updates, button-triggered state changes. They replace most of what `pages/api/` did, and they integrate with the cache so a write can invalidate the right pages without an extra round trip.

## Mark actions with `"use server"`

A server action is a function whose body begins with `"use server"`, or a module whose first line is `"use server"`. The directive promotes every exported function in the module to an action.

```ts
// app/notes/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function createNote(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  if (!title) throw new Error("title required");
  await db.note.create({ data: { title } });
  revalidatePath("/notes");
}
```

A file in a client component (`"use client"`) can still import an action; the framework swaps the function body for an RPC stub at build time. The implementation never ships to the browser.

## Pass FormData, not ad-hoc objects

A `<form action={serverAction}>` calls the action with a `FormData` instance and works without JavaScript. The browser submits the form; the framework reads the body server-side; the action runs. This is the progressive-enhancement path that ordinary `<button onClick>` cannot give you.

```tsx
// app/notes/new/page.tsx
import { createNote } from "../actions";
export default function NewNote() {
  return (
    <form action={createNote}>
      <input name="title" required />
      <button type="submit">Save</button>
    </form>
  );
}
```

For non-form invocations (a button that toggles a flag, an optimistic UI update), call the action like a normal function from a client component. See [[frontend/react-forms]] for the broader form pattern.

## Validate every input on the server

A server action runs with the user's privileges, but the framework does no type checking on the incoming `FormData`. Validate every field. Use Zod or another schema library so the validation is one statement, not a wall of `String(...)`. See [[frontend/react-forms]] for the FormData and Zod pattern at the form layer.

```ts
"use server";
import { z } from "zod";

const NoteInput = z.object({
  title: z.string().min(1).max(200),
  body: z.string().max(10000).optional(),
});

export async function createNote(formData: FormData) {
  const parsed = NoteInput.safeParse({
    title: formData.get("title"),
    body: formData.get("body"),
  });
  if (!parsed.success) return { error: parsed.error.flatten() };
  await db.note.create({ data: parsed.data });
  revalidatePath("/notes");
}
```

Treat the action as a public endpoint. Anyone who can render the page can call the action with arbitrary input; sanitize and authorize accordingly.

## Authorize before you write

A server action has access to cookies and headers via `next/headers`. Read the session at the top of the action and short-circuit on failure.

```ts
"use server";
import { cookies } from "next/headers";

export async function deleteNote(id: string) {
  const session = await getSession(await cookies());
  if (!session) throw new Error("unauthorized");
  await db.note.delete({ where: { id, ownerId: session.userId } });
  revalidatePath("/notes");
}
```

The action runs server-side, but the call originates from the client. Never trust a hidden form field for the user id or the resource owner.

## Revalidate with `updateTag`, `revalidateTag`, or `revalidatePath`

A mutation that changes cached data must invalidate the cache. The wrong choice is to drop caching; the right choice is to tag what you cache and call the tag on write.

```ts
import { revalidatePath, revalidateTag } from "next/cache";
import { updateTag } from "next/cache";

updateTag("notes");                    // Cache Components: immediate, read-your-writes
revalidateTag("notes", "minutes");     // Cache Components: eventual, needs a cacheLife profile
revalidatePath("/notes");              // one route
revalidatePath("/notes/[id]", "page"); // a dynamic segment
```

On a project with `cacheComponents` enabled, prefer `updateTag` inside a server action so the user sees their own write immediately; `revalidateTag` is for content other users should eventually see fresh. On a project without Cache Components, `revalidateTag`/`revalidatePath` behave as in Next.js 15. Tag fetches, `"use cache"` functions, and `unstable_cache` calls so the action can invalidate them in one call. The full cache model lives in [[frontend/nextjs-caching]].

## Return values, not redirects, for inline UI

A server action's return value is available to a client component via `useActionState`. Use it for inline errors and success states.

```tsx
"use client";
import { useActionState } from "react";
import { createNote } from "./actions";

export function NoteForm() {
  const [state, action, pending] = useActionState(createNote, null);
  return (
    <form action={action}>
      <input name="title" />
      <button disabled={pending}>Save</button>
      {state?.error && <p>{state.error.formErrors.join(", ")}</p>}
    </form>
  );
}
```

Call `redirect("/notes")` only when the next paint should be a different route. A redirect after a mutation throws a special control-flow error; do not catch it.

## Pick actions over route handlers for mutations

A `route.ts` handler is for HTTP endpoints with custom status codes, public APIs, webhooks, or non-React clients. For a mutation a React component triggers, server actions win on type safety, integration with the cache, progressive enhancement, and brevity. See [[frontend/nextjs-route-handlers]] for the cases where a handler is the right tool.

## Related

- [[frontend/nextjs]]
- [[frontend/nextjs-app-router]]
- [[frontend/nextjs-caching]]
- [[frontend/nextjs-route-handlers]]
- [[frontend/react-forms]]
- [[frontend/react-server-components]]
- [[backend/postgres]]
