Галоўная / Артыкулы / Дзеянні сервера чыра обробнікі маршрутаў? Кансультатывны парад для Next.js 16

Дзеянні сервера чыра обробнікі маршрутаў? Кансультатывны парад для Next.js 16

Дазвольце дазнацься, калі мутацыя Next.js 16 паслужыць для Server Action, а калі ёй трэба Route Handler, з правіленымі прыкладамі для форм, адзінакоў, оптымізма і webhooks.

1558 слоў

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 для адкорекцый трэба выканаць у момент пераходу.

Спадні матэрыялы

  • Чаму Next.js Server Actions патрабуюць автарызацію ў кожным тэле функцыі — Детальны аналіз прыклада захоплення акаунта паказвае, як Next.js Server Actions без автарызаціі дазволяюць выкананне прывілейованных операцый, і дзе неабходна рэалізацыя пераканальніка автарызаціі, каб гэта запобiec.
  • Практычны падход да апгрэйда React 19: Калькі новыя API заменяюць існуючыя способы — Практычны аналіз викорыстання Server Actions, useOptimistic і React Compiler, з важлівымі застерэжэннямі та планамі ўсвайомлення першых змян у React 19.
  • Дзе трэба практычная лінія сервер-кліент у сторанцы Next.js App Router — практычны ментальны модэль для React Server Components: што запускаецца дзе, як сторанца блога дзеліцца на серверную і кліентскую часты, а таксама правіла імпорту, якія трэба дапэўнаваць.
  • Next.js чы проста React? Выбір фрэймворку для вашага проекту — падход, які спачатку аналізуе трэбаванні, каб выявіць, чы роўныму новы проект на React патрэбен Next.js, з урахоўваннем SEO, гібрыднага атрыбутавання, меры хостінгу, патрэбаў у маршрутацыі і дазнанняя каманды.
  • Едынага адказвальная сурэч для маршрутных шляхоў у прыкладнасцях Next.js — Дазвольце дазнайсца, як модуль центральных маршрутаў з падтрымкай типаў заменяе розрозненыя стрынговыя шляхі ў Next.js, а таксама як выкарыстоўваюцься дапаможныя функцыі для дынамічных сегментаў і групавання экспортаў маршрутаў.
  • Чаму WebSockets застаюцца у стане очаквання ў маршрутах API Next.js і як цэе лячыць спецыяльны сервер — Дазвольце дазнайсца, чаму сервер ws унутры маршрута pages/api ніколі не завершае процэс узаемадзейнавання, і як самостайна кераваць падзеяй апграду самым спецыяльным серверам Next.js.