Next.js server actions для более чистых границ команды
Используйте Next.js server actions, чтобы оставить формы лёгкими, перенести проверки auth на сервер и не дать доменным правилам расползаться по React-компонентам.

Почему команды прячут логику в компонентах
Большинство команд не планируют смешивать продуктовые правила с кодом React. Так получается потому, что первая версия работает, а главное — успеть выпустить. Форма начинается с простых проверок полей, потом кто-то добавляет ещё одно условие, потом ещё одно, и вскоре компонент сам решает, кто что может, когда и по какой цене.
Обычно всё начинается с хороших намерений. У человека, который делает экран, уже есть кнопка, текущий пользователь и данные формы прямо перед глазами. Добавить маленькое правило внутри компонента кажется быстрее, чем создавать для него отдельное место. В стартапе или небольшой команде такой обходной путь может сэкономить час сегодня и стоить неделю через месяц.
Именно на формах это быстрее всего превращается в хаос. Форма приглашения может начать с «email обязателен» и «нужно выбрать роль». Потом к ней добавляются лимиты тарифа, проверки доступных мест в workspace, правила одобрения админом и ограничения trial-периода. Ни одно из этих правил на самом деле не связано с отрисовкой полей, но они часто оказываются рядом с JSX, потому что именно там всё началось.
Аутентификация делает разрастание ещё хуже. Команда добавляет одну проверку auth, чтобы скрыть кнопку, другую — в загрузчик страницы, и ещё одну — в кастомный хук. Через несколько спринтов никто уже не доверяет единому источнику правды. Один человек меняет правило для менеджеров на странице, другой — в модальном окне, и оба думают, что всё исправили.
Вот почему небольшие изменения UI могут ломать поведение продукта. Дизайнер просит перенести форму в drawer, разбить страницу на вкладки или переиспользовать кнопку в новом месте. Команда думает, что это безопасная правка фронтенда. А потом исчезает ценовое правило, перестаёт срабатывать ветка согласования или пользователь видит действие, до которого он вообще не должен добираться.
Команды часто приходят к Next.js server actions после того, как почувствуют эту боль. Само по себе это не решение. Решение — провести чёткую границу: компоненты собирают ввод и показывают состояние, а business logic в React перестаёт расползаться по скрытым углам.
Проведите границу между UI и правилами продукта
React-компонент должен заниматься тем, что пользователь видит и с чем взаимодействует. Это значит состояние полей, состояние загрузки, фокус, неактивные кнопки и способ показа ошибок у полей. Если пользователь ввёл неправильный email, компонент может подсветить поле красным. Если отправка не удалась, компонент может показать сообщение в правильном месте.
Продуктовые правила должны жить в другом месте.
Когда форма решает, кого можно пригласить в команду, позволяет ли текущий тариф добавить ещё места или заблокирован ли email-домен, UI начинает тащить в себе business logic в React. Сначала это кажется быстрым. Через месяц то же правило появляется ещё в одной форме, в API route и на админ-экране — и везде с небольшими отличиями.
Next.js server actions дают команде чистое разделение, если использовать их дисциплинированно. Server Action должен обрабатывать аутентификацию, проверки прав, запись в базу и проверки правил, которые должны оставаться верными независимо от того, какой экран их вызывает. Компонент собирает ввод и отображает результат. Action выполняет защищённую работу.
Само правило не стоит держать прямо в action, если вы можете переиспользовать его. Вынесите его в обычную функцию с понятным названием. Например, canInviteMember(actor, workspace) или validateInvite(email, workspace, plan) говорит намного больше, чем блок условий, спрятанный в JSX. Тогда server action становится тонким слоем, который читает сессию, вызывает эти функции, записывает данные и возвращает результат.
Такое разделение обычно выглядит так:
- Компонент управляет полями, фокусом и локальным показом ошибок.
- Server Action проверяет, кто пользователь и что он может делать.
- Простые функции хранят продуктовые правила.
- Слой базы данных сохраняет изменения после прохождения проверок.
Хороший тест очень простой. Спросите: «Это правило всё ещё имело бы значение, если бы формы не было?» Если ответ да, вынесите его из компонента. Спросите: «Это правило всё ещё имело бы значение, если бы server action изменился?» Если снова да, вынесите его в переиспользуемую функцию и дайте action вызывать её. Так формы остаются маленькими, а правила — легко находимыми.
Что должно жить в Server Action
Server Action должен делать ту работу, которой браузеру нельзя доверять. Клиент может собрать ввод и показать состояние загрузки. Сервер должен решать, кто пользователь, разрешён ли запрос и безопасно ли сохранять данные.
Начинайте с доверенных данных сессии. Считывайте подписанную сессию на сервере и берите оттуда реального пользователя, аккаунт или workspace. Не доверяйте скрытым полям для userId, role или workspaceId. Пользователь может изменить эти значения в браузере за секунды.
Потом проверяйте весь payload, прежде чем что-то записывать. Проверяйте базовую структуру и типы, но также и правила, которые затрагивают несколько полей сразу. Если форма отправляет пустое имя, диапазон дат в неправильном порядке или сумму вне допустимого лимита, останавливайте запрос сразу. Частичные сохранения создают грязные баги, а команда потом тратит часы на их исправление.
Оставляйте action тонким
Action должен подготавливать запрос, а не хранить у себя всю продуктовую логику. После auth и валидации вызывайте одну доменную функцию, которая принимает реальное решение. Эта функция должна отвечать на бизнес-вопрос: может ли этот человек создать запись, изменить статус или пригласить этого пользователя?
Для команды такое разделение очень важно. Frontend-разработчики могут связывать формы и обратную связь, не трогая глубокие правила. Разработчики с backend-уклоном могут менять доменные правила в одном месте. Если позже у вас появится API route, фоновая задача или админ-инструмент, вы сможете переиспользовать ту же функцию вместо того, чтобы копировать логику из React-компонента.
Возвращайте небольшой результат, который UI сможет сразу показать. Обычно это значит флаг успеха, короткое сообщение, несколько ошибок по полям или id обновлённой записи. Делайте ответ простым и предсказуемым. Компонент не должен разбирать огромный объект ответа, чтобы показать toast или отметить одно поле как ошибочное.
Хорошие Next.js server actions часто читаются почти одинаково: получить сессию, распарсить ввод, вызвать доменную функцию, вернуть компактный результат. Если action начинает по строчкам решать ценообразование, права доступа и пограничные случаи, значит эту логику пора опустить на слой ниже.
Простой поток для форм, authentication и правил
Next.js server actions работают лучше всего, когда путь скучный и его легко проследить. Пользователь заполняет обычную форму, форма отправляет именованные поля в один action, а action решает, что будет дальше. Звучит просто, но именно это не даёт команде смешивать клики по кнопкам, проверки прав и продуктовые правила в одном React-файле.
Начинайте с похожих на HTML полей, которые соответствуют нужным вам данным. Если форма создаёт проект, поля вроде name, visibility и ownerId читать легче, чем вложенный объект клиентского состояния, который пробрасывается через три компонента. UI должен собирать ввод и показывать обратную связь. Он не должен решать, кому разрешено создать проект и нарушает ли название проекта правило компании.
Чистый поток обычно выглядит так:
- Форма отправляет именованные поля в один server action.
- Action определяет пользователя и загружает его роль или членство в workspace.
- Action передаёт чистый ввод в доменную функцию.
- Доменная функция проверяет продуктовые правила и возвращает либо ошибку, либо одобренные данные.
- Action сохраняет изменение и возвращает небольшой результат, например
ok,messageили ошибки по полям.
Порядок важен. Сначала проверяйте, кто отправил запрос, и только потом запускайте бизнес-правило. Пользователь без входа в систему или без прав должен проваливаться сразу. Так остальной код остаётся сосредоточенным на самом правиле, а не смешивает auth-проверки с каждым условием.
Потом держите доменное правило в отдельной функции, даже если сегодня оно занимает всего десять строк. Возможно, правило говорит, что у workspace может быть только один billing owner, или что на free-плане нельзя создавать private projects. Такие правила должны жить в коде, который можно вызвать из server action, задачи или админ-инструмента. Им не место внутри компонента формы.
Когда правило проходит, сохраняйте изменение и возвращайте простой статус. Держите ответ небольшим. Большинству форм достаточно данных, чтобы показать успех, ошибку или обновить состояние страницы. Если action начинает возвращать половину модели приложения, граница уже размывается.
Пример: приглашение коллеги в workspace
Поток приглашения хорошо показывает эту границу. React-форма должна делать одну маленькую работу: собрать email и роль, а затем отправить данные. Она может отключить кнопку на время запроса и не создавать лишнего шума на экране. Но она не должна решать, кто может приглашать, какие роли разрешены и хватает ли в workspace места ещё на одного человека.
В setup на основе Next.js server actions action становится воротами между формой и правилами продукта. Он читает вошедшего пользователя из сессии, проверяет, к какому workspace относится запрос, и подтверждает, что пользователь может управлять участниками. Если базовая проверка доступа не проходит, action останавливается ещё до того, как запустится логика приглашения.
После этого action должен вызвать доменную функцию. Эта функция владеет правилами, важными для бизнеса, а не для страницы. Например, она может отклонить попытку назначить роль только для owner, остановить повторное приглашение, если email уже является участником или уже ожидает подтверждения, и заблокировать запрос, когда команда достигла лимита мест.
Такое разделение делает каждую часть простой:
- Компонент собирает email и роль.
- Server Action проверяет личность и доступ к workspace.
- Доменная функция применяет правила приглашения.
- Страница показывает финальный результат.
Результат, который приходит в UI, должен быть простым и удобным. Часто достаточно такой структуры, как { ok: false, message: "Мест больше нет" }. Когда action завершается, страница может показать одно понятное сообщение рядом с формой вместо того, чтобы смешивать toast-уведомления, ошибки полей и скрытые изменения состояния.
Представьте, что тимлид приглашает [email protected] как «Owner». Форма принимает ввод, потому что ввод email и выбор роли — это не решение по безопасности. Server Action подтверждает, что тимлид вошёл в систему и относится к workspace. Затем доменная функция отклоняет запрос, потому что назначать эту роль может только текущий владелец workspace. UI показывает одно прямое сообщение, например: «Только владелец workspace может назначать эту роль». Это кажется мелочью, но именно так продуктовая логика перестаёт утекать в компоненты и копироваться по страницам.
Как организовать файлы, чтобы правила оставались видимыми
Когда команды внедряют Next.js server actions, они часто складывают всё в компонент формы, потому что так быстрее. Через месяц компонент проверяет сессию, парсит поля, ищет записи, применяет лимиты тарифа и отправляет письма. Уже никто не понимает, какие строки существуют ради UI, а какие защищают продукт.
Более чистая структура разделяет файлы по задаче. Держите форму рядом со страницей, которая её использует, потому что это UI-деталь. Перенесите action в небольшой файл actions.ts рядом с этой страницей, потому что action — это входная точка. А реальные правила положите в другое место, обычно в папку services или use-cases, потому что эти правила должны пережить изменения формы.
Простой шаблон папок
page.tsxзагружает данные и рендерит экранInviteMemberForm.tsxобрабатывает поля, локальное состояние и ошибки полейactions.tsчитает form data, проверяет пользователя и вызывает бизнес-функциюservices/workspaces/invite-member.tsприменяет правила workspace
Именно последний файл важнее всего. Он должен отвечать на бизнес-вопросы вроде: Может ли этот админ приглашать людей? Осталось ли доступное место? Нужно ли системе блокировать повторные приглашения? Эти решения не должны жить в React.
Имена значат больше, чем многие думают. submitForm ничего не объясняет. inviteTeammateToWorkspace сразу показывает бизнес-шаг. checkPlanLimit понятнее, чем validate. Если ревьюер может быстро просканировать названия функций и понять поток, кодовая база остаётся читаемой даже при росте команды.
Auth тоже должен оставаться тонким. Action может получить текущего пользователя и передать его id в доменную функцию. А доменная функция уже должна решить, может ли этот пользователь выполнить шаг. Так правила авторизации живут в одном месте, а не размазаны по компонентам.
Небольшая команда может долго использовать простую структуру вроде этой. Oleg применяет ту же общую идею в AI-first delivery work: держать точки входа маленькими, правила — явными, а каждой файлу — одну задачу. Такой подход помогает легче находить баги и труднее прятать продуктовые решения.
Ошибки, которые команды совершают с Server Actions
Команды часто переносят код в Next.js server actions, но оставляют прежнее плохое разделение обязанностей. Файл меняется, но мышление — нет. Продуктовые правила всё так же прячутся в обработчиках клика, компонентах формы и разрозненных хелперах.
Одна из частых ошибок — помещать цены, согласования или лимиты тарифа прямо в обработчики кнопок. Кнопка может решать, когда отправлять форму. Но она не должна решать, может ли workspace добавить ещё одно платное место или нужно ли приглашению одобрение менеджера. Когда эта логика живёт в компоненте, другой экран либо скопирует её плохо, либо не скопирует вообще.
Права доступа — ещё одно слабое место. Клиентское состояние полезно, чтобы показывать или скрывать UI, но оно никогда не должно решать, что может делать пользователь. Если страница говорит, что человек — админ, потому что в памяти остались старые данные, action всё равно обязан проверить реальную сессию, workspace и роль на сервере. Считайте клиент удобством, а не доказательством.
Валидация тоже быстро начинает расползаться. Команды пишут одни и те же проверки в форме, потом ещё раз в одном action, потом ещё раз в другом action, который обрабатывает то же правило с другой страницы. Через месяц один путь принимает плохое значение, а другой его отклоняет. Общую валидацию держите в одной небольшой доменной функции и вызывайте её из каждого action, которому она нужна.
Ещё одна ошибка — возвращать огромные объекты, когда UI нужен лишь результат вроде ok, error или invite_sent. Большие payload усложняют чтение action и увеличивают шанс ошибиться. Большинству форм нужен статус, иногда сообщение и, в некоторых случаях, один небольшой кусок данных.
Ненужный вред наносят и слишком общие хелперы. Хелпер с названием handleWorkspaceAction(), который пишет в базу, проверяет права, ставит flash-состояние и делает redirect, может выглядеть аккуратно, но он скрывает реальное поведение. Когда teammate читает action, он должен видеть запись, проверку правила и redirect без необходимости разбирать три файла.
Более чистый шаблон простой:
- держите состояние формы и показ полей в компоненте
- проверяйте auth и права внутри action
- помещайте бизнес-правила в простые доменные функции
- возвращайте самый маленький результат, который нужен UI
- делайте redirects и записи легко заметными
Если teammate не может после беглого чтения понять, где живёт правило, граница всё ещё размыта.
Краткий чеклист для ревью
Когда вы ревьюите PR с Next.js server actions, смотрите в первую очередь на одно: можно ли после 30 секунд чтения понять, где на самом деле живёт продуктовое правило? Если для этого приходится читать JSX, обработчики кнопок и код состояния формы, граница уже размыта.
Хорошее ревью — это меньше про стиль и больше про размещение кода. Команды обычно попадают в неприятности, когда форма начинается с малого, а потом внутри компонента появляются проверки прав, лимиты цены и правила статуса, потому что так быстрее здесь и сейчас.
Используйте короткий чеклист:
- Читатель может указать на один файл, где живёт правило. Если правило звучит как «только владельцы workspace могут приглашать коллег», оно должно находиться в одной понятной доменной функции или модуле, а не наполовину в компоненте и наполовину в action.
- Компонент занимается только вводом и обратной связью. Он собирает значения формы, показывает состояние ожидания и отображает сообщения об успехе или ошибке. Он не должен решать, кому что разрешено.
- Action проверяет личность и затем вызывает доменный код. Action — это серверная точка входа, поэтому он должен проверить пользователя, прочитать доверенный контекст и передать чистые данные в правило.
- Две формы могут переиспользовать одно и то же правило без копипаста. Если и модальное окно, и страница настроек приглашают коллег, обе должны вызывать одну и ту же доменную функцию.
Один маленький тест помогает. Представьте, что завтра меняется одно продуктовое правило. Например, редакторы могут приглашать гостей, но не участников. Сколько файлов нужно менять? Один — это здорово. Два — ещё может быть нормально. Пять — значит, правило утекло в UI-код.
Так ревью тоже становится быстрее. Teammate может открыть компонент и увидеть поля формы. Потом открыть action и увидеть auth. Затем открыть доменный файл и увидеть бизнес-правило в обычном коде. У каждого файла своя задача.
Такое разделение звучит жёстко, но оно экономит время. Команды, которые сохраняют эту границу, реже ошибаются с правами доступа, больше переиспользуют код и меньше спорят о том, куда класть новую логику.
Что менять первым в вашем кодовой базе
Начните с одной формы, которая уже раздражает. Выберите экран, где React-компонент рендерит поля, проверяет права, тримит ввод, пишет в базу и решает, какое сообщение об ошибке показать. Такие формы есть почти в каждой команде. Форма приглашения — очень частый пример.
Не пытайтесь переделать всё приложение за один проход. Измените один путь — от клика до сохранения. Этого достаточно, чтобы показать команде более чистое разделение и доказать, что Next.js server actions могут уменьшать путаницу, а не добавлять ещё один слой.
Сначала вынесите одно продуктовое правило из компонента. Оставьте его простым. Например, «только админы workspace могут приглашать пользователей» не должно жить в JSX. Поместите это правило в небольшую функцию с понятным названием. Потом пусть UI собирает ввод и показывает результат.
Хороший первый рефакторинг выглядит так:
- держите компонент сфокусированным на полях, состоянии загрузки и сообщениях
- создайте один server action для одного намерения пользователя, например приглашения коллеги
- вызывайте простую функцию для доменного правила, например проверки прав или мест
- возвращайте простые данные об успехе и ошибке, которые форма может показать
Такое разделение кажется маленьким, потому что оно и есть маленькое. И это хорошо. Огромный action под названием saveWorkspaceSettings обычно превращается в ящик с хламом. Один action на одно намерение легче тестировать, легче ревьюить и легче менять, когда продуктовые правила меняются.
Если ваша команда постоянно ломает эту границу, проблема редко только в дисциплине. Возможно, кодовая база просто не даёт людям очевидного места для правил. Oleg Sotnikov часто помогает командам исправить именно эту проблему как Fractional CTO или advisor. Короткий разбор вашей архитектуры Next.js может показать, почему business logic снова и снова утекает в компоненты, и какая структура папок и action-ов это остановит.
Выберите на этой неделе одну проблемную форму. Вынесите одно правило. Переименуйте один action так, чтобы его назначение стало очевидным. После этого следующий рефакторинг пойдёт намного легче.