On this page
Server Actions let a Next.js application connect form submissions to server-side functions without designing a separate endpoint for every interaction. They do not make submitted values trustworthy or eliminate the need for authorization. Treat an action as a remotely callable mutation boundary with an explicit contract.
Define the mutation, not just the handler
Write the operation in domain terms: rename a project, invite a member, or archive a document. Identify who may perform it, which values are editable, and what must change afterward. Mutation boundaries are easier to review when an action performs one coherent operation.
Use the JSON Formatter to inspect a sanitized example of the result object your action will return. Keep it small: a status, known field errors, or a safe message. Do not return a database exception or a complete user record merely because the framework can serialize a result.
The action should establish identity on the server, validate input, authorize the resource, commit the change, and then update relevant rendered data. Calling those steps from a server file does not imply they happened automatically. A hidden form field is still controlled by the requester.
Parse FormData at the boundary
FormData values can be strings or files, and missing fields return null. Narrow values before applying application rules. Avoid converting everything with String immediately: String(null) produces a nonempty string that can obscure a missing value.
// app/projects/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { requireUser, renameOwnedProject } from '@/lib/projects';
type Result = { ok: boolean; message: string };
export async function renameProject(formData: FormData): Promise<Result> {
const user = await requireUser();
const id = formData.get('id');
const rawName = formData.get('name');
if (typeof id !== 'string' || typeof rawName !== 'string') {
return { ok: false, message: 'Project and name are required.' };
}
const name = rawName.trim();
if (name.length < 1 || name.length > 120) {
return { ok: false, message: 'Use a name between 1 and 120 characters.' };
}
await renameOwnedProject({ userId: user.id, projectId: id, name });
revalidatePath('/projects');
return { ok: true, message: 'Project renamed.' };
}The imported helpers are application-specific. requireUser must obtain a trusted session, and renameOwnedProject must enforce ownership or the relevant permission in the data operation. Authorization close to data prevents a caller from replacing the hidden identifier with someone else's project.
The Next.js updating data guide describes form integration and action invocation. Keep validation responses serializable and safe to display. Expected user-correctable failures should not depend on exposing stack traces from thrown exceptions.
Connect form state without losing intent
React's useActionState can associate a result with a form action and expose pending state. The wrapped action receives previous state before the submitted payload, so adapt the server function signature intentionally. A function written to accept FormData alone is not automatically interchangeable with that reducer-style signature.
Use pending feedback to show that a submission is in progress, but do not mistake a disabled button for a complete duplicate-prevention strategy. Retries, multiple tabs, and repeated requests can still occur. Operations with financial or irreversible consequences need an appropriate server-side idempotency design.
Keep user-entered values available when validation fails. Provide a form-level summary for general failures and associate field-specific messages with their controls. A message that appears visually but is disconnected from the field can be difficult to discover with assistive technology.
Read the React useActionState reference for the installed React version. Progressive enhancement depends on how the form and action are composed; wrapping everything in client-only imperative submission can discard benefits that native action forms would otherwise provide. Test the actual route before claiming no-JavaScript support.
Revalidate only after a committed change
A successful write does not guarantee that every cached view immediately reflects it. Identify the affected route or tagged data, then choose the current Next.js revalidation API that matches that ownership. Write completion and rendered-data freshness are related but distinct concerns.
Start with a targeted revalidatePath when the route is the natural scope. More sophisticated applications may use tagged data and tag-based invalidation. Check the documentation for the installed Next.js version because cache defaults and tag invalidation APIs have evolved. Avoid copying an older snippet without verifying its semantics.
Redirect only when navigation is the desired result. In Next.js, redirect uses framework control flow; do not bury it in a broad catch that turns successful navigation into an error. Handle expected database failures around the write, then perform framework navigation or revalidation in a clear sequence.
The revalidatePath reference explains what it affects. Test a detail view and its parent list together. Renaming a project may update the detail heading while leaving a list, breadcrumb, or account summary stale if those views use different data paths.
Review security and failure boundaries
Treat every action argument as untrusted, including bound values and identifiers passed from a Client Component. The framework's transport protections do not replace permission checks. Verify the caller can still perform the operation at execution time, not only when the page originally rendered.
Do not trust a client-supplied price, role, or account owner. Load authoritative values on the server. Server authority means the action derives sensitive decisions from trusted data and the authenticated identity. Validation checks shape and range; authorization checks whether this user may make this change.
Test a signed-out call, an expired session, a foreign resource identifier, missing fields, and an unexpected database failure. Ensure internal errors produce safe user messages and useful private diagnostics. Revalidation should not happen after a rolled-back write, and a successful response should not conceal a partial multi-step transaction.
Use the React collection and development guides for adjacent patterns, while keeping each action's contract close to its implementation. Small actions with explicit dependencies are easier to audit than a generic action dispatcher that accepts arbitrary operation names and payloads.
Handle a lost response after a successful write
Consider an invitation action that creates a membership invitation and sends an email. The database commit succeeds, but the browser loses its connection before receiving confirmation. The user sees uncertainty and submits again. A disabled button could not prevent this because the first request has already completed on the server.
Assign a stable operation identifier to this submission intent and bind it to the authenticated account. Enforce uniqueness server-side and store enough result information to return a consistent answer for a replay. Reject reuse of the same identifier with a different payload. A fresh random identifier generated for every retry defeats this protection because each attempt appears to be a new operation.
Email delivery adds another boundary: a database rollback cannot unsend a message. A transactional outbox can record the invitation and the pending notification together, then let a worker deliver it with its own duplicate-handling policy. Do not promise exactly-once delivery merely because the action returns once.
Test the ambiguous outcome by allowing the write to finish and then dropping the response. On retry, verify that only one invitation exists, the result remains understandable, and the refreshed membership list reflects the committed state.
Key takeaways
- Authenticate, parse, authorize, and write within the server-side mutation boundary.
- Return safe validation state and preserve user input after expected failures.
- Revalidate affected views after commit, and test unauthorized calls, duplicate requests, and framework navigation behavior.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor