Skip to main content
Назад к блогу
5 мин чтения

Server Actions в продакшне: 4 вещи, которые я больше не повторю

Server Actions упрощают мутации в Next.js. Но после шиппинга на трёх клиентских проектах я собрал четыре паттерна, которые выглядели нормально в разработке и ломались в продакшне.

next.js

Server Actions — одна из лучших вещей в современном Next.js. Одна функция, 'use server', никаких API-роутов, никакого fetch-бойлерплейта. Я использую их на каждом проекте.

Но у них есть острые края. После шиппинга Server Actions на трёх клиентских проектах — вот четыре паттерна, которые я больше не повторю.

1. Тяжёлые вычисления внутри экшна

Server Actions выполняются на сервере — очевидно. Что менее очевидно — они работают в том же процессе, что обслуживает страницы. Server Action, который выполняется 3 секунды, блокирует воркер на 3 секунды.

На проекте с генерацией инвойсов Stripe я засунул рендеринг PDF прямо в Server Action. В разработке работало отлично (один пользователь, без конкаренси). В продакшне три пользователя, генерирующих инвойсы одновременно, замедлили всё приложение. Загрузка страниц деградировала, другие Server Actions встали в очередь за PDF-работой.

Исправление: Server Actions должны быть тонкими. Они валидируют вход, пишут в базу и тригерят фоновую работу. Генерация PDF переехала в фоновую задачу (простая очередь на BullMQ). Server Action теперь пишет строку invoice_requested, возвращается немедленно, а воркер подхватывает.

'use server'

export async function generateInvoice(formData: FormData) {
  const data = invoiceSchema.parse(Object.fromEntries(formData))

  // Не делай так:
  // const pdf = await renderInvoicePDF(data) // 2-4 секунды

  // Делай так:
  await db.invoiceJobs.create({ data, status: 'pending' })
  revalidatePath('/invoices')
}

Правило: если занимает больше 500мс — не место в Server Action.

2. Пропуск валидации входных данных

«Это серверная сторона, можно не валидировать.» Слышал это от других разработчиков. И сам так думал — ненадолго.

Server Actions — публично доступные эндпоинты. Кто угодно может вызвать их с любым payload. Директива 'use server' создаёт HTTP-эндпоинт под капотом. Если ты доверяешь форме данных, потому что «форма отправляет только валидные данные» — ты доверяешь клиенту, что прямо противоположно тому, что должен делать серверный код.

На одном проекте Server Action принимал поле role из формы и писал его прямо в базу. В форме был дропдаун с «user» и «admin». В разработке — нормально. В продакшне кто-то через DevTools отправил role: "superadmin" — роль, которая существовала в базе, но не была в UI.

Исправление: валидируй каждый вход Server Action через Zod (или аналог). Не только типы — бизнес-правила тоже.

'use server'

const updateRoleSchema = z.object({
  userId: z.string().uuid(),
  role: z.enum(['user', 'editor']), // Только разрешённые роли
})

export async function updateRole(formData: FormData) {
  const { userId, role } = updateRoleSchema.parse(Object.fromEntries(formData))
  // Теперь безопасно писать
}

Правило: Server Actions — это API-эндпоинты. Относись к ним как к API-эндпоинтам.

3. Использование Server Actions для получения данных

Этот паттерн я вижу в туториалах и сам использовал однажды, прежде чем осознал стоимость.

// Не делай так
'use server'
export async function getUsers() {
  return db.users.findMany()
}

// В клиентском компоненте:
const [users, setUsers] = useState([])
useEffect(() => {
  getUsers().then(setUsers)
}, [])

Это работает. Но теряет смысл Server Components. Ты шипишь клиентский компонент, который фетчит данные через Server Action, когда мог бы иметь Server Component, который фетчит данные напрямую — без клиентского JS, без стейта загрузки, с поддержкой стриминга.

Паттерн выше также имеет неочевидную проблему производительности: данные проходят через сериализацию Server Action (React Flight protocol), что добавляет оверхед по сравнению с прямым вызовом базы в Server Component.

Когда Server Actions должны фетчить данные: когда нужен рефетч после мутации в клиентском компоненте, и revalidatePath/revalidateTag недостаточно гранулярны. Всё.

// Делай так: Server Component фетчит напрямую
async function UsersPage() {
  const users = await db.users.findMany()
  return <UserList users={users} />
}

Правило: Server Components фетчат данные. Server Actions мутируют данные. Разделяй их.

4. Забываем ревалидацию

Server Actions мутируют данные. После мутации закэшированная страница по-прежнему показывает старые данные. Если забыть ревалидировать — пользователи видят устаревшее состояние до хард-рефреша.

Я зашипил форму комментариев, где Server Action вставлял комментарий, но не вызывал revalidatePath. Пользователь отправил комментарий, форма очистилась (успех!), но список комментариев не обновился. Он отправил снова. И снова. Три дубликата, прежде чем он обновил страницу.

Исправление: каждый Server Action, который пишет данные, должен ревалидировать. Без исключений.

'use server'

export async function addComment(formData: FormData) {
  const data = commentSchema.parse(Object.fromEntries(formData))
  await db.comments.create({ data })

  // Не забудь:
  revalidatePath(`/posts/${data.postId}`)
}

Две стратегии ревалидации:

Теперь у меня есть линт-правило, которое отмечает Server Actions с вызовами db. или prisma. без соответствующего revalidatePath или revalidateTag. С момента добавления оно поймало три пропущенные ревалидации.

Паттерн, который работает

После этих уроков каждый Server Action у меня следует одной структуре:

'use server'

export async function doThing(formData: FormData) {
  // 1. Валидация
  const data = schema.parse(Object.fromEntries(formData))

  // 2. Авторизация
  const session = await auth()
  if (!session) throw new Error('Unauthorized')

  // 3. Мутация (быстро)
  await db.things.create({ data })

  // 4. Ревалидация
  revalidatePath('/things')
}

Четыре шага, по порядку, каждый раз. Валидация, авторизация, мутация, ревалидация. Если мутация медленная — шаг 3 становится «поставь фоновую задачу в очередь» вместо выполнения работы инлайн.

Server Actions — отличный примитив. Им просто нужна та же дисциплина, что и любому другому API-эндпоинту — потому что это именно то, чем они являются.


Строишь на Next.js App Router и хочешь избежать продакшн-подводных камней? Напиши — я шипил Server Actions на нескольких клиентских проектах и знаю, где острые края.