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 на кількох клієнтських проєктах і знаю, де гострі краї.