A Server Action sits next to a React form, so it can feel like an internal function. It is still a server entry point reachable from a client. A user can modify form fields, repeat requests, and call an action without using the button you designed.
The safe mental model is straightforward: treat every exported Server Action like a mutation endpoint. Authenticate the caller, authorize the exact object and operation, validate all input, control expensive work, and return only data intended for the browser.
authorize inside the action
Hiding an edit button does not protect an action. Middleware that checks whether a session exists is also insufficient because it does not know whether this user owns this project.
"use server";
export async function renameProject(formData: FormData) {
const session = await requireSession();
const input = RenameProject.parse({
projectId: formData.get("projectId"),
name: formData.get("name"),
});
const project = await db.project.findFirst({
where: { id: input.projectId, ownerId: session.userId },
select: { id: true },
});
if (!project) return { ok: false, error: "not_found" };
await db.project.update({
where: { id: project.id },
data: { name: input.name },
});
revalidatePath(`/projects/${project.id}`);
return { ok: true };
}
Putting ownership in the query reduces the chance of fetching an unauthorized object and passing it deeper into the service. Administrative operations should also check a named permission, not only a role string copied across files.
parse form data into a strict schema
FormData values can be strings or files, and fields may be absent or repeated. Convert that untrusted shape at the boundary:
const RenameProject = z.object({
projectId: z.string().uuid(),
name: z.string().trim().min(1).max(80),
}).strict();
Do not spread form data or arbitrary objects into an ORM update. Construct the update from allowed properties so a caller cannot add ownerId, role, or isApproved.
Validation answers whether data has an acceptable shape. Authorization answers whether this actor may use it. Both checks are required.
keep sensitive data out of the return value
Only return the fields the interface needs. Database rows can contain email addresses, internal notes, recovery tokens, billing identifiers, or authorization fields that should never be serialized to the client.
Use stable public error codes rather than raw database or provider messages:
type RenameResult =
| { ok: true }
| { ok: false; error: "invalid_input" | "not_found" | "rate_limited" };
Log detailed errors on the server with a correlation ID. Show a neutral message to the user. Never return stack traces, SQL errors, or secret-bearing provider responses.
assume requests can repeat
Double clicks, browser retries, slow network behavior, and malicious clients can submit a mutation more than once. For a harmless rename, repetition may be acceptable. For checkout, invitation email, credit use, or withdrawal, use an operation ID and durable idempotency protection.
CREATE UNIQUE INDEX checkout_operation_once
ON checkout_operations (account_id, operation_key);
Do not rely on disabling the submit button. That improves UX but does not protect server state. Keep irreversible side effects behind a transaction, outbox, or provider idempotency key as the workflow requires.
limit abuse and expensive work
Server Actions are not exempt from rate limits. Protect login, password reset, invitations, exports, AI generation, file processing, and money-related actions according to their cost and abuse case.
Apply body and file-size limits before expensive parsing. Move slow work to a queue after recording a durable operation. Set timeouts for external services and avoid launching unbounded parallel requests from one action.
The limit key may combine session account, user ID, source IP, and action name. Be careful when trusting forwarded IP headers behind proxies.
use redirects and revalidation deliberately
revalidatePath and revalidateTag affect cached data; they do not authorize it. Revalidate only after the durable change succeeds. Avoid broad invalidation when one narrow tag is enough, because a cheap mutation could otherwise create expensive regeneration work.
Validate redirect destinations. Do not redirect directly to a user-provided URL without restricting it to known internal paths, or the action can create an open redirect useful in phishing.
protect dependencies around the action
Cookies should use HttpOnly, Secure, and an appropriate SameSite policy. Follow the current Next.js data security guidance, because framework behavior and recommendations can change. Keep Next.js and React security releases current.
Server-only modules should not be imported into client components. Mark sensitive modules with server-only where useful and keep secret-reading code behind a small server boundary.
log an audit-friendly decision
For security-relevant actions, log the action name, actor ID, object ID, decision, reason code, request correlation ID, and deployment version. Do not log full form bodies by default.
Metrics should distinguish validation failures, authorization denials, rate limits, dependency failures, and successful mutations. A spike in not_found for objects owned by other users can indicate object-ID probing even if every request was denied.
write negative tests first
A useful test matrix includes:
| Test | Expected result |
|---|---|
| No session | Authentication failure |
| User B edits user A’s project | No data change, neutral denial |
| Extra protected property in input | Validation failure |
| Repeated operation key | One side effect |
| Oversized file or field | Rejected before processing |
| Provider timeout | Durable recoverable state |
| Invalid redirect destination | Refused or normalized internally |
Call the action through the same transport the browser uses where possible. Unit tests of a helper do not prove that route wiring, serialization, and session boundaries are correct.
Server Actions reduce plumbing, not responsibility. The security standard remains the same as any API mutation: distrust the request, prove permission at the object boundary, make repetition safe, and leave enough evidence to investigate a bad outcome.
Discussion
What would you try, change, or challenge after reading this guide? Specific results and errors help the next reader.
Comments will load as you reach this section.