In the previous tutorial, you learned how to read data in Next.js 16. Real applications also create posts, edit profiles, change settings, and delete records. Those operations are mutations: they change application state.
This guide follows the current Next.js 16 App Router model. You will build form actions, read FormData, validate input, check authorization, report pending and error states, update cached data, redirect safely, and understand when optimistic UI is worth using.
Updating Data at a Glance
The browser submits named form controls. A Server Function receives their FormData, validates untrusted values, verifies the current user, performs a parameterized database operation, and then refreshes or redirects the interface.
- Read dataBlog 6
- Submit form
- Server Action
- Validate
- Update database
- Revalidate
- Updated UI
What Are Server Actions?
The current documentation uses Server Function for an async function that runs on the server and can be called from the client through a network request. In an action or mutation context, it is also called a Server Action. “Next.js Server Actions” remains the common search term, but Server Function is the broader React concept.
When a Server Action is passed to a form's action prop or a button's formAction prop, React automatically runs it in a Transition. Next.js invokes actions with POST and can return updated UI and data in one server roundtrip. This is ideal for application-owned mutations, but it is not a universal replacement for HTTP APIs.
Read
- Page
- Server Component
- Database or API
- Render data
Write
- Form
- Server Action
- Validate + authorize
- Database mutation
- Revalidate or redirect
Understanding "use server"
The directive has two current patterns. An inline directive is convenient when an action belongs to one Server Component:
export default function PostsPage() {
async function createPost(formData: FormData) {
'use server'
// Validate, authorize, mutate, revalidate
}
return <form action={createPost}>...</form>
}A module-level directive marks every exported async function in that file as a Server Function. Use this when Client Components must import actions or several routes share them:
'use server'
export async function createPost(formData: FormData) {
// Server-only mutation
}
export async function deletePost(formData: FormData) {
// Server-only mutation
}"use server" keeps execution on the server. It does not prove the caller's identity, validate values, or grant permission to change a record. Treat every exported action as a reachable mutation endpoint.
Your First Server Action
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
export async function createPost(formData: FormData) {
const rawTitle = formData.get('title')
if (typeof rawTitle !== 'string' || rawTitle.trim().length < 3) {
return { ok: false, message: 'Title must be at least 3 characters.' }
}
await db.posts.create({ title: rawTitle.trim() })
revalidatePath('/posts')
return { ok: true, message: 'Post created.' }
}The repository is a PHP content site and contains no Next.js database package, ORM, validation library, or auth implementation. For that reason, examples use a deliberately generic db interface rather than pretending Prisma, Drizzle, or another package is installed. Adapt the method to your own parameterized data layer.
Using Server Actions with Forms
import { createPost } from '@/app/posts/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<label htmlFor="title">Title</label>
<input id="title" name="title" required minLength={3} />
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required />
<button type="submit">Create post</button>
</form>
)
}The name attributes determine the keys in FormData. Server Component forms also support progressive enhancement: they can submit before JavaScript loads. Keep native labels, required fields, and browser validation, but repeat validation on the server because client constraints can be bypassed.
Reading and Validating FormData
FormData.get() can return a string, a File, or null. Narrow each value before string operations. Avoid blindly using Object.fromEntries(formData); React may add internal fields, multiple values can be collapsed, and uploaded files need separate rules.
type PostInput = { title: string; content: string }
function parsePost(formData: FormData):
| { ok: true; data: PostInput }
| { ok: false; errors: Record<string, string> } {
const title = formData.get('title')
const content = formData.get('content')
const errors: Record<string, string> = {}
if (typeof title !== 'string' || title.trim().length < 3) {
errors.title = 'Use at least 3 characters.'
}
if (typeof content !== 'string' || content.trim().length < 20) {
errors.content = 'Use at least 20 characters.'
}
if (Object.keys(errors).length) return { ok: false, errors }
return { ok: true, data: { title: title.trim(), content: content.trim() } }
}Creating, Updating, and Deleting Data
Create and update actions share the same sequence: validate values, authenticate, authorize the operation, mutate through a safe data API, and refresh affected views. Updates and deletes also need a record identifier, but never trust that identifier as proof of ownership.
'use server'
import { revalidatePath } from 'next/cache'
import { verifySession } from '@/lib/auth'
import { db } from '@/lib/db'
export async function updatePost(id: string, formData: FormData) {
const session = await verifySession()
if (!session) return { ok: false, message: 'Sign in required.' }
const parsed = parsePost(formData)
if (!parsed.ok) return { ok: false, errors: parsed.errors }
const post = await db.posts.findById(id)
if (!post || post.authorId !== session.user.id) {
return { ok: false, message: 'You cannot edit this post.' }
}
await db.posts.update(id, parsed.data)
revalidatePath(`/posts/${id}`)
return { ok: true, message: 'Post updated.' }
}
export async function deletePost(id: string) {
const session = await verifySession()
if (!session) return { ok: false, message: 'Sign in required.' }
const post = await db.posts.findById(id)
if (!post || post.authorId !== session.user.id) {
return { ok: false, message: 'You cannot delete this post.' }
}
await db.posts.delete(id)
revalidatePath('/posts')
return { ok: true }
}Use parameterized ORM or driver methods. Do not concatenate a client-supplied ID or title into SQL. For destructive controls, clearly label the action, require deliberate confirmation where the impact is meaningful, disable duplicate submissions, and consider audit logging or a recoverable soft delete.

Authentication and Authorization
Authentication answers “who is making this request?” Authorization answers “may that user perform this specific operation?” A hidden Edit button improves the interface but is not enforcement. Repeat both checks inside the action, close to the database access.
- Verify session
- Load target record
- Check role or ownership
- Allow mutation
- Reject safely
Do not infer ownership from a hidden userId field. Read the user ID from the verified server session, then compare it with the stored record. Return a safe message for expected denial; avoid exposing queries, stack traces, tokens, or internal table names.
Pending State with useFormStatus
A small Client Component can expose the parent form's submission status without turning the whole route into client code. This keeps data access and mutation logic server-side, matching the boundaries explained in Blog #5.
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending} aria-disabled={pending}>
{pending ? 'Saving...' : 'Save post'}
</button>
)
}useFormStatus must run in a component rendered inside the relevant form. Disable repeat submission, keep the pending label understandable, and preserve a touch target of at least roughly 44 pixels.
Expected Errors with useActionState
Return validation and business errors as serializable state. Throw unexpected failures so the nearest error boundary can handle them. With useActionState, the action receives previous state first and the form payload second.
For route boundaries, 404 states, safe retries, digests, and production logging, continue with the dedicated Next.js 16 error handling guide.
// actions.ts
'use server'
type State = { message: string; errors?: Record<string, string> }
export async function createPost(
previousState: State,
formData: FormData
): Promise<State> {
const parsed = parsePost(formData)
if (!parsed.ok) return { message: 'Check the form.', errors: parsed.errors }
// Authenticate, authorize, and write through the data layer.
return { message: 'Post created.' }
}
// post-form.tsx
'use client'
import { useActionState } from 'react'
import { createPost } from './actions'
const initialState = { message: '' }
export function PostForm() {
const [state, formAction, pending] = useActionState(createPost, initialState)
return (
<form action={formAction}>
<input name="title" aria-describedby="title-error" />
<p id="title-error" role="alert">{state.errors?.title}</p>
<button disabled={pending}>{pending ? 'Creating...' : 'Create'}</button>
<p aria-live="polite">{state.message}</p>
</form>
)
}The state returned from a Server Function must be serializable. Return plain strings, numbers, booleans, arrays, and plain objects rather than database clients, class instances, functions, secrets, or raw error objects.
Revalidating Updated Data
A successful database write does not always update every cached view automatically. Pick the smallest invalidation that matches how the data was cached:
revalidatePath('/posts')invalidates a specific route path.updateTag('posts')is a Next.js 16 Server Action API for immediate read-your-writes behavior.revalidateTag('posts', 'max')marks tagged data stale and uses stale-while-revalidate semantics, useful when a slight delay is acceptable.refresh()refreshes the client router from a Server Action but does not invalidate tagged data.
'use server'
import { revalidatePath, revalidateTag, updateTag } from 'next/cache'
export async function publishPost(formData: FormData) {
// Validate, authorize, and persist first.
revalidatePath('/posts')
updateTag('my-posts') // Immediate read-your-writes
revalidateTag('public-posts', 'max') // Stale-while-revalidate
}revalidatePathupdateTagrevalidateTag(tag, 'max')For a deeper explanation of cache lifetimes, tags, path invalidation, and stale data, read our Next.js 16 caching and revalidation guide.
Redirecting After a Mutation
Call redirect() after the write and required revalidation. It throws a framework-handled control-flow exception, so keep it outside a try block that would catch it. In a Server Action it produces a 303 response.
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
let postId: string
try {
postId = await saveValidatedPost(formData)
} catch {
return { message: 'The post could not be created.' }
}
revalidatePath('/posts')
redirect(`/posts/${encodeURIComponent(postId)}`)
}Prefer a known internal route built from a validated identifier. Do not pass an arbitrary user-supplied URL straight into redirect. For more navigation context, see Next.js linking, navigation, and redirects.
Passing Additional Arguments
Use bind when the action needs a trusted contextual value plus FormData. The bound value becomes an argument before the form payload:
const updatePostWithId = updatePost.bind(null, post.id)
return <form action={updatePostWithId}>...</form>A hidden input is visible in HTML and is equally untrusted. Bound arguments are not an authorization mechanism either: always look up the target and verify access server-side.
Calling Server Actions from Client Components
You cannot define a Server Function inside a Client Component. Import one from a dedicated module with a top-level "use server" directive, or receive it through a prop whose name ends in Action. Forms are usually the simplest path. Event handlers are supported too; when invoking an action outside an Action prop, use a Transition so React can coordinate pending and optimistic state.
Optimistic Updates with useOptimistic
Normal UI waits for server confirmation before showing the final state. Optimistic UI shows the likely result immediately while the action runs, then reconciles with the authoritative response. It works best for frequent, reversible interactions such as likes or adding a lightweight list item. Avoid it for destructive or high-stakes writes unless rollback behavior is exceptionally clear.

Normal
- User action
- Server roundtrip
- Confirmed UI
Optimistic
- User action + instant UI
- Server roundtrip
- Confirm or recover
'use client'
import { useOptimistic } from 'react'
import { addComment } from './actions'
export function Comments({ comments }: { comments: Comment[] }) {
const [optimisticComments, addOptimistic] = useOptimistic(
comments,
(current, pending: Comment) => [...current, pending]
)
async function formAction(formData: FormData) {
const rawBody = formData.get('body')
if (typeof rawBody !== 'string') return
const body = rawBody.trim()
addOptimistic({ id: 'pending', body, pending: true })
await addComment(formData)
}
return <form action={formAction}>{/* render list and controls */}</form>
}Complete CRUD Architecture
A maintainable feature separates route rendering, small interactive controls, action orchestration, validation, authorization, and persistence. That structure reduces accidental client exposure and lets each protected action reuse the same policy.
app/
|-- posts/
| |-- actions.ts # 'use server', mutation orchestration
| |-- page.tsx # read and render posts
| |-- new/page.tsx # route-level create form
| `-- [id]/edit/page.tsx # route-level edit form
|-- ui/posts/
| |-- post-form.tsx # focused client state only if needed
| `-- submit-button.tsx # useFormStatus
lib/
|-- auth.ts # session verification
|-- posts.ts # authorized data operations
`-- validation.ts # shared input rulesServer Actions vs Route Handlers
| Need | Often suitable |
|---|---|
| Form mutation inside a Next.js app | Server Action |
| Public HTTP endpoint | Route Handler |
| Webhook endpoint | Route Handler |
| Third-party API consumer | Route Handler or API |
| Server-side form mutation | Server Action |
| External clients or mobile apps | HTTP API or Route Handler |
These are tendencies, not absolute rules. Blog #10 will cover Route Handlers in detail.
Common Mistakes
- Trusting the client. Repeat validation, authentication, and authorization in every protected action.
- Using a client ID as ownership. Load the record and compare it with the verified server session.
- Making the whole page a Client Component. Isolate pending, status, and optimistic interaction.
- Using the old one-argument
revalidateTag. In Next.js 16 use a profile such as'max', or useupdateTagfor immediate action-owned updates. - Catching
redirect(). Redirect aftertry/catchbecause it throws to control framework flow. - Returning raw errors. Return safe expected messages and let an error boundary handle unexpected failures.
- Assuming "use server" makes code secure. It only defines the execution boundary.
- Building SQL strings. Use parameterized methods from the chosen driver or ORM.
Best Practices
- Keep mutation functions async and server-only.
- Prefer semantic forms when the interaction is a submission.
- Use native constraints for usability and server validation for trust.
- Verify session, permission, role, and ownership inside the action.
- Return minimal serializable state to Client Components.
- Choose path and tag invalidation deliberately after a successful write.
- Disable duplicate submission and announce progress or safe errors accessibly.
- Use optimistic state only when failure recovery is understandable.
- Keep redirects internal or validate destinations with an allowlist.
- Protect destructive actions with confirmation and server-side policy.
Frequently Asked Questions
What are Server Actions in Next.js 16?
A Server Action is an asynchronous React Server Function used for an action or mutation, such as a form submission. Server Function is the broader official term.
What does "use server" do?
It marks an async function, or every exported async function in a module, as callable server-side. It does not replace input validation, authentication, or authorization.
How do I submit a form with a Server Action?
Import or define a Server Function and pass it to the React form action prop. React supplies the submitted FormData as the action argument.
How do I get form values in a Server Action?
Use FormData.get or FormData.getAll, then narrow the unknown values and validate them before using them.
Can Server Actions update a database?
Yes. Keep database access in server-only code, use parameterized ORM or driver methods, and perform authorization before every protected write.
Do Server Actions need authentication?
Protected actions do. Treat each action like a public-facing endpoint and verify the session and the user permission inside the action.
What is the difference between validation and authorization?
Validation checks whether input has an acceptable shape and value. Authorization checks whether the current user may perform that operation on the selected record.
How do I refresh data after a Server Action?
Use revalidatePath for a path, updateTag for immediate read-your-writes tag invalidation in a Server Action, or revalidateTag with a cache-life profile for stale-while-revalidate behavior.
Can I call a Server Action from a Client Component?
Yes. Import it from a module with a top-level use server directive, receive it as a prop, or invoke it from a supported action or event flow.
Should I use Server Actions or Route Handlers?
Server Actions often suit mutations inside a Next.js UI. Route Handlers often suit public HTTP APIs, webhooks, mobile clients, and third-party consumers.
Official Resources
- Next.js: Updating Data
- Next.js:
use server - Next.js:
revalidatePath - Next.js:
revalidateTag - Next.js 16 upgrade guide:
updateTagand caching APIs - Next.js:
redirect - React:
useActionState - React DOM:
useFormStatus - React:
useOptimistic
Next Steps
You can now move from reading data to secure mutations: forms call Server Actions, actions validate and authorize, the data layer writes safely, and revalidation or redirects bring the interface up to date. Continue with Next.js 16 caching and revalidation to control freshness precisely.
