Next.js / Basic: routes and execution boundaries / Lesson 6
Forms, Server Actions and validated mutations
Treat a Server Action as a callable mutation boundary with untrusted input, current authority and a deliberate result contract.
On this page
Lesson goals
- Explain form submission, server function references and pending/error state.
- Validate and authorize inside the action rather than relying on client UI.
A server directive is not an access-control rule
A Server Action connects an explicit user mutation to server execution, often through a form. use server marks the framework boundary but does not make caller input trustworthy or check whether the current principal owns the object. Treat actions as reachable server entry points. A hidden course ID, bound argument or client-supplied revision still needs validation and policy. Keep the coherent business operation in a service that can enforce transaction and replay rules. A function’s server placement and the framework’s invocation machinery do not guarantee exactly-once effects or safe automatic retries after a lost response.
Validate form values and resolve current authority inside
The illustrative action reads FormData, obtains the current principal and calls a domain service. FormData.get can return a string, a file-like value or null, so a text validator cannot assume every value is a string. Imports and helpers are intentionally omitted and the action was not compiled or submitted. Return a small expected result for validation or workflow rejection without exposing internal exceptions. Use the selected React form-state integration when a client needs pending or field errors, and keep the signature aligned with that hook rather than copying a one-argument form handler unchanged.
export async function renameCourse(form:FormData) {
"use server";
const principal=await currentPrincipal();
const input=parseRenameForm(form);
return catalog.renameFor(principal,input);
}
Form parsing, principal construction and catalog behavior are injected assumptions. No Server Action request or mutation occurred.
Keep pending state and canonical results meaningful
Native form semantics and progressive enhancement can make submission useful before all client code arrives, but the actual behavior depends on where the action is defined and integrated. A disabled pending button improves interaction and does not prevent duplicate requests from every caller. useActionState can expose returned state and pending status under its React contract. Distinguish expected field problems from unexpected source failures, and render accessible feedback associated with inputs. Reconcile the canonical server result rather than assuming optimistic text exactly matches stored normalization or current revision. A redirect or revalidation step is separate from successful durable domain work.
Reject nontext and blank values locally
The local title parser accepts only a string, trims outer whitespace and enforces a small code-point bound. It rejects null, a synthetic object and blank text without writing anything. The range is a training contract, not a universal editorial limit. Counting code points does not count user-perceived grapheme clusters or storage bytes. This plain function can be reused behind a FormData adapter, but its tests do not prove the adapter selected the correct field, authenticated the principal or kept an unknown owner property away from persistence. Keep each of those boundaries explicit.
function renameTitle(value) {
if (typeof value !== "string") throw new Error("INVALID_TITLE");
const title=value.trim();
if ([...title].length < 1 || [...title].length > 60) throw new Error("INVALID_TITLE");
return title;
}
console.assert(renameTitle(" Actions ") === "Actions");
for (const value of [null,{}," "]) {let rejected=false;try {renameTitle(value);} catch {rejected=true;} console.assert(rejected);}
The valid string is normalized and all three invalid values reject. No FormData submission, React hook or server action was run.
Separate commit, refresh and redirect outcomes
Validate input and current object authority before the effect, then apply the chosen transaction and request-identity protocol. Invalidate relevant data only after a confirmed outcome and choose whether the user needs stale-while-revalidate or immediate read-your-own-write semantics. Redirect uses framework control flow and should not be swallowed by a catch-all that converts it into a generic failure. Test validation with no write, denied access, duplicate intent, conflict and response loss after commit. This course runs only the title parser; action transport, progressive enhancement, hook state, redirects and actual cache updates remain unexecuted.
Common errors and fixes
A hidden courseId authorizes the rename
- Why it happens
- Caller-controlled form data was trusted as permission.
- How to fix it
- Resolve current identity and object policy inside the action/service.
- Prevent it next time
- Test direct forged input with an ineligible principal.
A file-like value becomes an unintended title
- Why it happens
- FormData values were stringified without type checks.
- How to fix it
- Validate actual text values and reject other shapes.
- Prevent it next time
- Test null, file-like objects and blank input.
A redirect is caught as a generic action failure
- Why it happens
- Framework control flow was swallowed by a broad catch.
- How to fix it
- Keep redirect outside the catch or follow documented handling.
- Prevent it next time
- Test expected navigation and source rejection separately.
Check your understanding
1. Does a pending disabled button guarantee one server effect?
Show worked solution
No. Callers can bypass the UI, retry after network loss or send concurrent requests. Use domain idempotency and revision checks where needed, and verify the authoritative result. Pending state is interaction feedback, not a durable uniqueness mechanism or a transaction boundary.
2. Why validate inside the action even if the page already checked access?
Show worked solution
The action is its own callable server boundary and current authority may differ from the earlier render. Inputs can be forged and objects can change. Recheck the principal and requested transition before effects, then share domain policy across every other entry point that can perform the same use case.
Key takeaways
- Server Actions require the same input and authority rigor as other server endpoints.
- Commit, UI feedback and cache refresh are distinct outcomes.