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}`)
}
Две стратегии ревалидации:
revalidatePath(path)— ревалидирует конкретный роут. Используй, когда мутация затрагивает одну страницу.revalidateTag(tag)— ревалидирует все фетчи с этим тегом. Используй, когда мутация затрагивает несколько страниц (например, изменение профиля пользователя, которое влияет на хедер на каждой странице).
Теперь у меня есть линт-правило, которое отмечает 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 на нескольких клиентских проектах и знаю, где острые края.