Дзеянні сервера чыра обробнікі маршрутаў? Кансультатывны парад для Next.js 16
Дазвольце дазнацься, калі мутацыя Next.js 16 паслужыць для Server Action, а калі ёй трэба Route Handler, з правіленымі прыкладамі для форм, адзінакоў, оптымізма і webhooks.
Server Actions пачаткаліся ў Next.js 13.4 як спосаб прышвартавання форм і зараз є асалютным элементам App Router, пры тым кожная новая функцыя задае тую ж запитанне: дзеяння чы ўжо API-маршрут? Хутчэй паказана ментальная модель для выбору, прыклады обох падходаў у кодзе і способы выправлення хыбных уявлень, якія вызываюць рэальныя багі.
Два разных контракты
API-маршруты, якія ў App Router называюцца Route Handlers (app/api/.../route.ts), — це звычныя HTTP-канцэнтры. У іх є стабільныя URL-адресы, якія можа вызваць будзь-який кліент, повны контроль над кодамі статусу і заголовкамі кэшавання, а таксама жаданая связь з вашымі компонентамі.
Server Actions — це функцыі з атрыбутам 'use server', якія запускаюцца з вашага коду на React. Next.js генеруе канцэнтр, серыялізуе аргументы і рэзультаты, а таксама прыўязвае іх да форм, пераходаў і оптымістичнага інтерфейсу, пры чым типы прайходзяць да месца вызову.
У працоўнасці з тым, калі часта стверджуецца, дзейства сервера не ўтрымліваюцца прыватна: кожна з іх — это канецчык POST, пазначанный ID дзейства, і будь-хто, у якога є гэты ID, можа выклікаты яго з будь-якімі аргументамі.
Загальны прынцып: зованні з званэй стороны адпавядаюць кэшэнтару маршрута; зміны, якія запускаюцца за дапамогою вашага власнага інтерфейсу, зазвычай падходзяць під дзейства сервера.
Дзейства сервера на практыцы
Зміна, якая адбываецца за дапамогою формы
Файл пачынаецца з дырэктывы, якая пазначае кожны экспорт як функцыю сервера:
// app/actions/order.ts
'use server'
Дзейство аутентыфікуе, пераканае поля, запішае замовленне, праканае знову /orders і перенаправіць. Функцыя auth() чытае кукі запиту, таму кліент не пасылае токен.
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'export async function createOrder(formData: FormData) {
// Auth is automatic — no need to pass session
const session = await auth()
if (!session?.user) throw new Error('Unauthorized') const item = formData.get('item') as string
const quantity = Number(formData.get('quantity')) // Validate
if (!item || quantity < 1) {
return { error: 'Invalid order data' }
} // Database write
const order = await db.order.create({
data: { item, quantity, userId: session.user.id },
}) // Invalidate cached data
revalidatePath('/orders') // Redirect after success
redirect(`/orders/${order.id}`)
}
Заўважыце, што redirect працюе праз кіданне аберанцэй, таму ніколі не выклікайте яго ў блоке try, який поглотвае аберанцэі. Старонка імпортуе гэта дзейство:
// app/shop/page.tsx
import { createOrder } from '@/app/actions/order'
і перадае яго безпосередзя ў атрыбут action формы:
export default function ShopPage() {
return (
<form action={createOrder}>
<input name="item" type="text" placeholder="Item name" />
<input name="quantity" type="number" defaultValue={1} />
<button type="submit">Order</button>
</form>
)
}
Няма стану кліента, эфекта чыра fetch; форма надсылае данні ўжо перш чым завантажыцца JavaScript.
Верненне памылак з useActionState
useActionState (React 19) зберагае апошню значэння, якое вернула дзеянне, і флаг або чакання:
'use client'
import { useActionState } from 'react'
import { createOrder } from '@/app/actions/order'
export function OrderForm() {
const [state, action, isPending] = useActionState(createOrder, null) return (
<form action={action}>
{state?.error && (
<p style={{ color: 'red' }}>{state.error}</p>
)}
<input name="item" />
<input name="quantity" type="number" />
<button type="submit" disabled={isPending}>
{isPending ? 'Ordering...' : 'Place Order'}
</button>
</form>
)
}
Адна проблема: калі дзеянне абгортваеся за дапамою useActionState, яно отрымае пярэдні стан як першы аргумент, а FormData — як другі. Функцыя createOrder, паказаная вышэй, прыме чыра formData, таму для гэтай формы яе трэба зменіць на createOrder(prevState, formData).
Оптымістычныя падказкі з useOptimistic
useOptimistic паказвае апэктыўны результат пакуль дзеянне не будзе завершана:
'use client'
import { useOptimistic } from 'react'
import { toggleLike } from '@/app/actions/post'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, setOptimistic] = useOptimistic(initialLikes) async function handleLike() {
setOptimistic(prev => prev + 1) // UI updates instantly
await toggleLike(postId) // Server call happens in background
} return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
)
}
Оптымістычны канстантар должен выконвацца ўнутрь пераходу або дзеяння. З звычайнага onClick трэба абгорнуць тэлы канстантара ў startTransition; інакш React паведамляе пра адзінакі, і оптымістычны стан не будзе працаваць як планавалося. Проблемы з вярненням да пачатковага стану раскладзены ў пяці способах адзінакоў функцыі useOptimistic.
Калі Route Handler — правы інструмент
Этот версіяваны канцэнтры паказвае ўсе яго прынады: параметры запиту, кэшаванне через CDN і аутентыкацыя за дапамогою API-клуча.
// app/api/v1/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const category = searchParams.get('category') const products = await db.product.findMany({
where: category ? { category } : undefined,
}) return NextResponse.json({ products }, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
}
})
}export async function POST(request: NextRequest) {
const apiKey = request.headers.get('x-api-key')
if (apiKey !== process.env.API_KEY) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
} const body = await request.json()
const product = await db.product.create({ data: body })
return NextResponse.json({ product }, { status: 201 })
}
Выберыце Route Handler, калі:
- мобільныя дапыткі викорыстоўваюць гэты канцэнтры;
- сторонняя структура надае webhooks;
- атрыбуты адпаведзей паказваюць патрэбныя заголовкі кэшавання;
- это ўжытковая, версіяваная API або продукт на базе API.
У працуючай сітцы трэба пераканаліць тэкст POST-запытку, а не працяваць з яймо без пераканалення.
Як адна способа параджуецца з іншай
- Паўторныя запыткі: у обох случаях ад браузера надходзіць адна запытка да вашага сервера; ніхто з ягох не ўжо ў формате сервер-до-сервера.
- Кліентскі пакет: логіка застаецца на сервере ў будзь-якім случае.
- Автаналіза: дзеяння автоматычна чытаюць інформацію сесіі з кукі; працоўнікі, які обслугоўваюць іншых кліентаў, пераканаліваюць токены аб ключы за кожной запыткай.
- Тыпы: інформацыя дзеляецца ад початку да канца з дзеяннямі; працоўнікам трэба кліент з адзначанымі типамі аб схемай.
- Кэшаванне: дзеяння анулююць кэш за дапамою
revalidatePathаб тагоў; працоўнікі задаюць значэнняCache-Control. - Оптымістычны інтерфейс: ўбудованы ў хукі React для дзеянняў; для працоўнікаў — ручны.
У момент напісання цього тексту Next.js таксама адмініструе Server Actions з боку кліента по адзіну, таму яны падходzą для мутацый, а не для паралельных чытанняў дадзеных; пераканайцеся ў актуальных інструкцыях.
Спіс пунктав для прыняцья рашэння
Пачніце з пытання, якое вяршыць большасць случаў:
Does an external system call this?
YES → API Route
Потым працавайце з рэштой:
Is this triggered by a form submit or user action within your UI?
YES → Server ActionDo you need explicit HTTP caching headers?
YES → API RouteDo you want automatic auth context without passing tokens?
YES → Server ActionDo you need to call this from a mobile app?
YES → API RouteEverything else?
→ Server Action (less boilerplate)
Два ошибкі, якіх трэба утрымацца
Направленне webhooks на Server Action
Stripe адправляе даныя на зарэгістраваны URL і не можа нацеліцца на ID дзеяння.
// ❌ Wrong — Server Actions can't receive arbitrary HTTP POSTs from Stripe
'use server'
export async function handleStripeWebhook() { ... }
Webhooks патрабуюцца ў Route Handler, дзе можна прачытаць неапрануты тэл і пераканацца ў падпісе.
// ✅ Correct
// app/api/webhooks/stripe/route.ts
export async function POST(request: NextRequest) { ... }
Даверлівасць до вхідных дадзеных дзеяння
Пакалі будь-яке дзеяння можа быць выклікана безпосередна, дзеяння на выдаленне без пераканань дазволяе будзь-каму выдаліць што завгодна:
// ❌ Wrong — Server Actions are not inherently trusted
'use server'
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } }) // Anyone can call this!
}
Рашэнне — пераканацца ў сесіі і праве власнасці пры чытанні, а пасля зноў пераверыць:
// ✅ Correct — always validate auth + ownership
'use server'
export async function deletePost(postId: string) {
const session = await auth()
const post = await db.post.findUnique({ where: { id: postId } })
if (post?.userId !== session?.user?.id) throw new Error('Forbidden')
await db.post.delete({ where: { id: postId } })
revalidatePath('/posts')
}
Таксама неабяжна чынна адклэска празьбам без сесій. Адзін раз прыгляніце нашую статэю па кантэксту автарызаціі ў кожным Server Action, якая дакладна раскрывае гэты тэму.
Ключовыя выводы
- Кожны Server Action ёсць доступны канчык маршруту; трэба аутентыфікацыя, автарызацыя і верыфікацыя якіх выпалююцца ўнутрь яго.
- За дапамогою
useActionStateакцыя спачатку отрымае пакананы стан. - Апэлі
useOptimisticдля адкорекцый трэба выканаць у момент пераходу.
Спадні матэрыялы
- useOptimistic Rollback: Пяць способаў абвалкання ў Next.js Server Actions — Дазвольце дазнацца, чаму useOptimistic тылу непазірна вяртае UI без адказаў паўтарыльнікам на проблемы, на прыкладзе пяці перапрацаваных способаў абвалкання ў Server Action і рабочага рашэння.