Server Actions в Next.js 16: полное руководство с примерами

Server Actions в Next.js 16 позволяют мутировать данные напрямую из компонентов без API-роутов. Разбираем формы, useActionState, валидацию Zod, useOptimistic, ревалидацию кэша и защиту от CSRF с рабочими примерами на React 19.

Server Actions в Next.js 16: гайд 2026

Обновлено: 3 июля 2026

Server Actions в Next.js 16 — это асинхронные функции с директивой "use server", которые исполняются только на сервере и вызываются напрямую из клиентских или серверных компонентов для мутации данных без отдельных API-роутов. В этом руководстве я разберу полный жизненный цикл Server Action: от простой формы до продвинутых паттернов с useActionState, useOptimistic, валидацией Zod и обработкой ошибок. Все примеры я проверил на реальном проекте с Next.js 16 и React 19, и там же собрал несколько граблей, на которые сам наступил.

  • Server Actions заменяют большинство сценариев с API Routes для форм и мутаций: код сервера и клиента живёт в одном файле.
  • Для форм используйте нативный атрибут action или новый компонент <form> с React 19: получите прогрессивное улучшение без JavaScript.
  • Хук useActionState (бывший useFormState) управляет ответом, ошибками валидации и pending-статусом.
  • Все входные данные валидируйте на сервере через Zod или Valibot. Доверять клиенту нельзя даже в Server Action.
  • Для мгновенного UI применяйте useOptimistic, а для инвалидации кэша, revalidatePath или revalidateTag.
  • Server Actions в Next.js 16 совместимы с Turbopack и получают автоматическую защиту от CSRF через Origin-заголовки.

Что такое Server Actions в Next.js 16

Итак, начнём с основ. Server Actions, это функции, объявленные с директивой "use server", которые Next.js автоматически превращает в защищённые POST-эндпоинты. При сборке фреймворк присваивает каждой функции уникальный идентификатор и генерирует клиентский стаб, отправляющий сериализованные аргументы по сети. Тело функции при этом никогда не попадает в клиентский бандл, а значит, вы можете обращаться к переменным окружения, базе данных и внутренним сервисам без риска утечки секретов.

В Next.js 16 (стабильный релиз апреля 2026 года) Server Actions вышли из «экспериментального» статуса и получили несколько важных улучшений: полную поддержку Turbopack в build-режиме, понятные сообщения об ошибках при передаче несериализуемых аргументов, автоматическую проверку заголовка Origin для защиты от CSRF и совместимость с React 19 Actions API. Директиву можно указать на уровне файла (тогда все экспорты становятся Server Actions) или на уровне отдельной функции.

По данным официального блога Next.js, к середине 2026 года более 70% новых проектов на App Router используют Server Actions вместо ручных Route Handlers для форм и мутаций. Это заметно сместило подход к архитектуре: слой контроллеров попросту исчезает, а бизнес-логика перемещается ближе к UI. Хорошо это или плохо, ещё поспорим, но экономия кода реальна.

Первое Server Action: обработка формы

Начнём с самого простого примера, формы создания заметки. В App Router вы можете передать Server Action напрямую в атрибут action HTML-формы. React 19 обеспечивает прогрессивное улучшение: форма отправится даже без включённого JavaScript.

// app/notes/new/page.tsx
import { redirect } from "next/navigation";
import { db } from "@/lib/db";

export default function NewNotePage() {
  async function createNote(formData: FormData) {
    "use server";

    const title = formData.get("title") as string;
    const body = formData.get("body") as string;

    const note = await db.note.create({
      data: { title, body, createdAt: new Date() },
    });

    redirect(`/notes/${note.id}`);
  }

  return (
    <form action={createNote}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">Создать</button>
    </form>
  );
}

Обратите внимание. Функция createNote определена внутри серверного компонента, но помечена директивой "use server". Это означает, что при сборке Next.js вынесет её в отдельный чанк и создаст безопасный endpoint. Функция redirect из next/navigation корректно работает внутри Server Action и вызывает клиентскую навигацию через специальный ответ сервера.

Для повторного использования лучше выносить действия в отдельный файл, например app/actions/notes.ts, с директивой "use server" в самом начале файла. Тогда любой компонент, клиентский или серверный, может импортировать и вызвать эту функцию.

Управление состоянием через useActionState

Как только форма выходит за рамки «отправить и забыть», нам нужен возврат данных: сообщения об ошибках, подтверждения, обновлённые значения полей. React 19 предоставляет хук useActionState (переименованный useFormState), который принимает Server Action и начальное состояние, а возвращает актуальное состояние и обёрнутое действие.

// app/actions/subscribe.ts
"use server";

type State = { ok: boolean; message: string };

export async function subscribe(
  prevState: State,
  formData: FormData
): Promise<State> {
  const email = formData.get("email");

  if (typeof email !== "string" || !email.includes("@")) {
    return { ok: false, message: "Некорректный email" };
  }

  await db.subscriber.create({ data: { email } });
  return { ok: true, message: "Подписка оформлена" };
}
// app/components/SubscribeForm.tsx
"use client";

import { useActionState } from "react";
import { subscribe } from "@/app/actions/subscribe";

const initialState = { ok: false, message: "" };

export function SubscribeForm() {
  const [state, formAction, pending] = useActionState(subscribe, initialState);

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <button disabled={pending}>
        {pending ? "Отправляем…" : "Подписаться"}
      </button>
      {state.message && (
        <p style={{ color: state.ok ? "green" : "red" }}>{state.message}</p>
      )}
    </form>
  );
}

Третий элемент кортежа, pending, избавляет от необходимости оборачивать компонент в useFormStatus. Форма автоматически блокирует кнопку на время запроса и восстанавливает состояние при получении ответа. Если вам нужен доступ к pending в дочерних компонентах, используйте useFormStatus: он читает контекст ближайшей формы.

Валидация данных с Zod

Честно говоря, ручные проверки полей быстро превращаются в лапшу условий. Библиотека Zod позволяет описать схему один раз и получить типизированные данные плюс структурированные ошибки. Это стандарт де-факто для валидации в Server Actions, и я лично в новых проектах уже не пишу проверки руками.

// app/actions/notes.ts
"use server";

import { z } from "zod";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

const NoteSchema = z.object({
  title: z.string().min(3, "Минимум 3 символа").max(120),
  body: z.string().min(10, "Заметка слишком короткая"),
  tags: z.string().optional(),
});

export type NoteFormState = {
  errors?: Record<string, string[]>;
  message?: string;
};

export async function createNote(
  _prev: NoteFormState,
  formData: FormData
): Promise<NoteFormState> {
  const parsed = NoteSchema.safeParse({
    title: formData.get("title"),
    body: formData.get("body"),
    tags: formData.get("tags"),
  });

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  try {
    const note = await db.note.create({ data: parsed.data });
    revalidatePath("/notes");
    redirect(`/notes/${note.id}`);
  } catch (e) {
    return { message: "Не удалось сохранить заметку" };
  }
}

Ключевая деталь: safeParse вместо parse. Он не выбрасывает исключение, а возвращает объединённый тип, где вы можете корректно ветвить логику. Метод flatten().fieldErrors превращает вложенные ошибки в плоскую карту «поле → массив сообщений», удобную для рендеринга под каждым инпутом.

Как обрабатывать ошибки в Server Actions

В Server Actions существует три категории ошибок, и каждая требует своего подхода. Ожидаемые ошибки бизнес-логики (невалидный ввод, дубликат, недостаточно прав) должны возвращаться как часть состояния, вы уже видели этот паттерн выше. Неожиданные ошибки (падение БД, таймаут) должны быть пойманы ближайшей границей error.tsx. А вот ошибки навигации (redirect, notFound) пробрасываются через React дальше.

// app/notes/error.tsx
"use client";

export default function NotesError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div role="alert">
      <h2>Что-то пошло не так</h2>
      <p>Идентификатор: {error.digest}</p>
      <button onClick={reset}>Попробовать снова</button>
    </div>
  );
}

Next.js 16 автоматически логирует stack trace на сервере и отправляет клиенту только сокращённое сообщение плюс digest. Это короткий хеш для сопоставления с серверными логами, и он же защищает от утечки внутренних деталей приложения.

Для более гранулярного контроля создайте типизированную обёртку. Наш стандартный подход, по логике похожий на директиву use cache для кэширования компонентов: возвращать { ok: boolean, data?, error? } из каждого действия, чтобы клиент получал предсказуемую форму.

Оптимистичные обновления через useOptimistic

Server Action по определению делает круговой путь до сервера, что даёт задержку в десятки и сотни миллисекунд. Хук useOptimistic позволяет мгновенно отрисовать «оптимистичное» состояние и откатить его, если действие завершится ошибкой.

// app/components/TodoList.tsx
"use client";

import { useOptimistic, startTransition } from "react";
import { toggleTodo } from "@/app/actions/todos";

type Todo = { id: string; text: string; done: boolean };

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state: Todo[], toggledId: string) =>
      state.map((t) =>
        t.id === toggledId ? { ...t, done: !t.done } : t
      )
  );

  async function handleToggle(id: string) {
    startTransition(() => {
      addOptimistic(id);
    });
    await toggleTodo(id);
  }

  return (
    <ul>
      {optimisticTodos.map((t) => (
        <li key={t.id}>
          <label>
            <input
              type="checkbox"
              checked={t.done}
              onChange={() => handleToggle(t.id)}
            />
            {t.text}
          </label>
        </li>
      ))}
    </ul>
  );
}

После завершения Server Action и последующей ревалидации данных React автоматически заменит оптимистичное состояние на реальное. Если действие упадёт, оптимистичное значение будет отброшено, и пользователь увидит исходное состояние. В отличие от старых подходов с локальным state, вручную писать логику отката вам не нужно.

Ревалидация кэша: revalidatePath и revalidateTag

После мутации данных серверные компоненты продолжат отдавать закэшированную версию, пока вы явно не инвалидируете кэш. Next.js предоставляет две функции. revalidatePath инвалидирует все данные, использованные на конкретном роуте, а revalidateTag инвалидирует данные, помеченные заданным тегом при fetch.

// app/actions/posts.ts
"use server";

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

export async function publishPost(id: string) {
  await db.post.update({ where: { id }, data: { published: true } });

  revalidatePath("/blog");
  revalidatePath(`/blog/${id}`);
  revalidateTag("posts");
}

Тег-ориентированная ревалидация мощнее. Вы можете помечать fetch("...", { next: { tags: ["posts"] } }) в любом месте приложения и инвалидировать сразу все связанные запросы одной командой. Для проектов с сотнями роутов это единственный поддерживаемый способ сохранить целостность кэша.

В Next.js 16 добавлена функция revalidatePath с поддержкой параметризованных сегментов. Вызов revalidatePath("/blog/[slug]", "page") инвалидирует все динамические экземпляры сразу. Подробности можно посмотреть в официальной документации revalidatePath.

Как вызвать Server Action из клиентского компонента

Server Actions вызываются из клиентских компонентов тремя способами. Первый: прямая передача в атрибут action формы (уже показан выше). Второй: обработчик события. Любой onClick, onChange или useEffect может выполнить действие как обычную асинхронную функцию.

"use client";
import { deletePost } from "@/app/actions/posts";

export function DeleteButton({ id }: { id: string }) {
  return (
    <button
      onClick={async () => {
        if (confirm("Удалить пост?")) {
          await deletePost(id);
        }
      }}
    >
      Удалить
    </button>
  );
}

Третий способ, .bind(), нужен для передачи дополнительных аргументов, которые не приходят из формы. Это важно, потому что action получает единственный аргумент FormData. С помощью bind можно «прикрепить» id, tenant или любой другой контекст безопасным способом. Значение сериализуется на сервере и подписывается, чтобы клиент не мог его подменить.

"use client";
import { updatePost } from "@/app/actions/posts";

export function EditForm({ postId }: { postId: string }) {
  const updateWithId = updatePost.bind(null, postId);
  return (
    <form action={updateWithId}>
      <input name="title" />
      <button>Сохранить</button>
    </form>
  );
}

Безопасны ли Server Actions

Server Actions генерируют публично доступные POST-эндпоинты, поэтому вопрос безопасности критичен. Next.js 16 включает три уровня защиты по умолчанию, но они не заменяют вашу собственную авторизацию.

CSRF-защита. Фреймворк автоматически сверяет заголовки Origin и Host. Если запрос приходит с другого домена, он отклоняется на уровне рантайма. Это блокирует классические CSRF-атаки без токенов.

Обфускация идентификаторов. Имена функций заменяются на криптографически случайные хеши при сборке, а неиспользуемые действия «мертвы»: их нельзя вызвать даже зная имя. Для дополнительной защиты в проде задайте encryptionKey в next.config.js, чтобы фиксировать ключ между деплоями.

Авторизация. Каждое действие должно самостоятельно проверять сессию. Server Action не «наследует» права страницы, из которой вызывается. Стандартный паттерн: вынести проверку в утилиту.

"use server";
import { auth } from "@/lib/auth";
import { forbidden } from "next/navigation";

async function requireUser() {
  const session = await auth();
  if (!session?.user) forbidden();
  return session.user;
}

export async function deletePost(id: string) {
  const user = await requireUser();
  const post = await db.post.findUnique({ where: { id } });
  if (post?.authorId !== user.id) forbidden();
  await db.post.delete({ where: { id } });
}

Функция forbidden() из next/navigation (появилась в 15.1) корректно отображает 403-страницу вместо бросания generic-ошибки. Я на прошлом проекте забыл её добавить и получил стектрейс в проде вместо аккуратного «доступ запрещён». С тех пор проверяю на каждом действии.

Server Actions против API Routes: когда что выбирать

Оба механизма создают серверные эндпоинты, но решают разные задачи. Таблица ниже сравнивает их по критериям, которые чаще всего влияют на выбор.

КритерийServer ActionsRoute Handlers (API)
Основной сценарийФормы, мутации из UIПубличные API, вебхуки
Совместное расположение с UIДа, в том же файлеНет, отдельный роут
Прогрессивное улучшениеРаботает без JSТребует fetch/JS
Поддержка методов HTTPТолько POSTGET, POST, PUT, DELETE и др.
CSRF-защитаАвтоматическиРеализуется вручную
Вызов из внешних клиентовНе предназначенДа
Ревалидация кэшаПрямой доступ к APIЧерез revalidatePath
Типизация «клиент-сервер»Совместная TypeScriptРучная (OpenAPI, tRPC)

Правило простое. Если действие вызывается только из вашего UI (форма, кнопка, обработчик), берите Server Action. Если endpoint должен потреблять мобильное приложение, вебхук платёжной системы или третья сторона, используйте Route Handler. Об этом подробнее в статье про кэширование компонентов в Next.js 16, где кэшируемые операции чтения тоже логически ближе к Server Components, чем к традиционным API.

Часто задаваемые вопросы

Можно ли использовать Server Actions без форм?

Да. Server Action, это обычная асинхронная функция, помеченная "use server". Её можно вызвать из любого обработчика клиентского компонента (onClick, onChange, useEffect) или напрямую из другого серверного компонента. Форма, лишь один из способов вызова.

Работают ли Server Actions с Turbopack в Next.js 16?

Да, в Next.js 16 Server Actions полностью совместимы с Turbopack и в dev-, и в build-режиме. Более того, скорость сборки действий с Turbopack выросла в 3–5 раз по сравнению со стандартным вебпаком, что особенно заметно в проектах с сотнями отдельных "use server" файлов.

Как передать заголовки или cookies из Server Action?

Используйте cookies() и headers() из next/headers. Внутри Server Action cookies().set() модифицирует cookie ответа, а cookies().get() читает cookie запроса. Это тот же API, что и в Server Components, только доступ к set разрешён.

Что делать, если Server Action возвращает несериализуемое значение?

Next.js 16 бросает понятную ошибку при возврате Date, Map, классов или циклических структур. Приводите данные к простым JSON-совместимым типам: даты в ISO-строки, Map в массив пар, объекты классов в плоские DTO. Файлы и Blob поддерживаются как аргументы, но не как возвращаемые значения.

Можно ли вызывать Server Action из внешнего приложения?

Технически endpoint существует, но CSRF-защита блокирует запросы с другим Origin. Server Actions не предназначены для внешних потребителей, для этого используйте Route Handlers с явной аутентификацией (API-ключи, OAuth). Попытка обойти защиту через spoofing заголовков нарушит работу защитных механизмов.

Editorial Team
Об авторе Editorial Team

Our team of expert writers and editors.