Insights · Nov 3, 2026 · 4 min read · FAQ

Server Actions or route handlers: where Next.js mutations belong

Server Actions suit changes made from your own UI; route handlers suit webhooks and outside callers. Both are public endpoints, so the checks go inside.

SERVER ACTIONSOr route handlers? SELLWRIGHT

Your Next.js app needs forms and data changes: sign-ups, settings, edits and deletes. The App Router offers two places for that code, Server Actions and route handlers. Here is how we choose, and the checks that keep both safe, because anyone who can send a request can call either.

What are Server Actions and route handlers?

A Server Action is an async function marked with the "use server" directive, which your interface calls like a normal function. Put the directive at the top of a function, or at the top of a file to mark every export. Server Components can define actions inline; Client Components can only import them from a "use server" file.

A route handler is a route.ts file that exports functions named after HTTP methods, such as GET and POST, using the Web Request and Response APIs. Its location is its address: app/api/route.ts answers requests to /api.

When should you use each one?

Use a Server Action for any change that starts in your own interface: saving a form, deleting a record or changing a setting. Pass it to a form's action prop or a button's formAction, or call it from an event handler. When it revalidates data, Next.js can return the result and the updated page in one round trip.

Use a route handler when something other than your own UI calls the server, or a URL must stay fixed. Action IDs are generated at build time and new deployments typically produce new ones, so outside callers cannot rely on them. Route handlers suit:

  • Webhooks from a CMS, CRM or payment provider.
  • Callback URLs, where a third-party sign-in or checkout sends the user back.
  • A public or partner API for a mobile app, a script or another system.
  • Responses that are not pages, such as an RSS feed or a file download.

To read data for your own pages, use neither: Server Functions are designed for mutations, and Next.js advises fetching in Server Components directly from the source.

How do you keep Server Actions secure?

Treat every Server Action as a public endpoint, because it is one. The Next.js docs warn that actions are reachable by a direct POST request, and that a page-level login check does not cover the actions on that page. Hiding a button changes what people see, not what they can send. Inside every action:

  1. Authenticate: read the session and stop if there is none.
  2. Authorise the specific record: being signed in is not permission to change this invoice. In a multi-tenant product that includes the tenant, as in keeping customer data separate.
  3. Validate input on the server with a schema library such as Zod or Valibot. A schema checks shape, not ownership.
  4. Accept an ID and the change, never the whole record.
  5. Return only what the screen needs, because return values are sent to the browser.
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { auth, db } from '@/lib/server'

const Rename = z.object({ id: z.string(), name: z.string().min(1).max(100) })

export async function renameProject(prevState: unknown, formData: FormData) {
  const session = await auth()
  if (!session?.user) return { error: 'Sign in again.' }

  const input = Rename.safeParse(Object.fromEntries(formData))
  if (!input.success) return { error: 'Check the name.' }

  const { count } = await db.project.updateMany({
    where: { id: input.data.id, ownerId: session.user.id },
    data: { name: input.data.name },
  })
  if (count === 0) return { error: 'Not found.' }

  revalidatePath('/projects')
  return { error: null }
}

Next.js adds its own layer: encrypted action IDs, unused actions stripped from the client bundle, an Origin and Host check against cross-site requests and a 1MB default body limit. The docs say these do not replace your checks. Route handlers are public endpoints too: verify credentials, validate payloads and check a webhook's secret first.

How does the page update after a change?

After the write, tell Next.js which cached data changed, using next/cache. revalidatePath invalidates one route. updateTag expires a cache tag so users see their own change at once; it works only in Server Actions. revalidateTag with the recommended 'max' profile refreshes a tag in the background and also works in route handlers, so it suits webhooks. refresh re-renders the current page without touching the cache. Call these before redirect, which throws, so nothing after it runs.

What do users see while it saves, or when it fails?

Forms that call Server Actions can work before JavaScript arrives. Next.js says forms in Server Components submit even if JavaScript hasn't loaded or is disabled, and Client Component forms queue the submission until the page hydrates. A form posting through client-side fetch gets none of this.

For feedback, use React's useActionState hook: it returns the latest state, an action for the form and a pending flag. Next.js recommends returning expected errors, like failed validation, as values. Unexpected errors reach the nearest error.tsx boundary.

Where to start

Sort every write in your app with four rules:

  • Your own form or button changes data: Server Action.
  • Another system calls you, or the URL must stay fixed: route handler.
  • A page only reads data: neither, fetch it in a Server Component.
  • Both paths touch the same data: keep authorisation and queries in one server-only data access layer that both call.

Then confirm each action and route handler checks the session, the record and the input. Our web app development service builds Next.js products this way. For a second opinion on an existing app, get in touch.

FAQ

Frequently asked questions

Are Server Actions the same as API routes?

No. API Routes belong to the older Pages Router, and route handlers are their equivalent in the App Router, so you do not need both. Server Actions are a different mechanism: React Server Functions that your interface calls through an ID Next.js generates at build time, rather than a URL you choose and document.

Why do users see "Failed to find Server Action" after a deploy?

Their open page still refers to action IDs from the previous build, and new deployments typically generate new ones. Next.js suggests rolling deployments when users are likely to be mid-change, a stable NEXT_SERVER_ACTIONS_ENCRYPTION_KEY shared across server instances, and treating the error as a retry in the UI so a refresh recovers the user.

Can we upload files through a Server Action?

Small files, yes, because an action accepts FormData, which can hold files. Action requests are capped at 1MB by default, and serverActions.bodySizeLimit raises that. For larger uploads, Next.js recommends a dedicated storage service: upload from the browser where possible and save the returned URL in your database.

Do we still need CSRF tokens with Server Actions?

Next.js covers most of it. Actions accept only POST, which the docs say prevents most CSRF problems in modern browsers now that SameSite cookies are the default, and Next.js also rejects requests whose Origin does not match the Host. If a proxy or CDN serves your app on another domain, list it in serverActions.allowedOrigins.

Is proxy.ts enough to protect our actions and routes?

No. proxy.ts, which some libraries still call middleware, can turn requests away early, but the Next.js docs say not to rely on proxy alone for authentication and authorisation. Keep a check inside every Server Action and route handler, ideally through a shared data access layer, so a path the matcher misses does not expose a write.

Next step

Ready for a website that sells?

Book a short call. We will look at your current site, agree what matters most and send a written scope with a fixed quote.

Book a callOr write to hello@sellwrightstudio.com