
Use a Server Action for server-side mutations initiated by your Next.js UI, especially forms that need authorization and cache revalidation. Use client fetch for live search, polling, upload progress, cancellation, and interactions that need explicit request control. If mobile apps or third parties need the same capability, design a Route Handler or independent backend API.
TOC
- Three request boundaries
- A form Server Action
- Client fetch
- Security is not automatic
- Cache revalidation
- When not to use a Server Action
- Decision checklist
- References
- A migration rule of thumb
- Common smell tests
Three request boundaries
| Approach | Best fit | Main limitation |
|---|---|---|
| Server Action | Internal forms and mutations | Not a general public API |
| Client fetch to Route Handler | Interactive browser requests | Requires an HTTP contract |
| Client fetch to a backend such as Strapi | A deliberately public backend API | Exposes the address and uses public/user credentials |
A form Server Action
// app/actions.ts
"use server"
import { revalidateTag } from "next/cache"
export async function createArticle(formData: FormData) {
const title = String(formData.get("title") ?? "").trim()
if (title.length < 3) {
return { ok: false, message: "Title must contain at least 3 characters" }
}
const user = await requireUser()
await saveArticle({ title, authorId: user.id })
revalidateTag("articles")
return { ok: true }
}
import { createArticle } from "@/app/actions"
export function ArticleForm() {
return (
<form action={createArticle}>
<input name="title" required minLength={3} />
<button type="submit">Create article</button>
</form>
)
}
Actions work naturally with forms and React's pending/result state APIs. Every input must still be validated on the server.
Client fetch
Live search is often clearer as a cancellable request:
"use client"
import { useEffect, useState } from "react"
export function SearchBox({ query }: { query: string }) {
const [results, setResults] = useState([])
useEffect(() => {
const controller = new AbortController()
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then((response) => {
if (!response.ok) throw new Error(`Search failed: ${response.status}`)
return response.json()
})
.then(setResults)
.catch((error) => {
if (error.name !== "AbortError") console.error(error)
})
return () => controller.abort()
}, [query])
return <ResultList results={results} />
}
Client fetch supports cancellation and granular response handling, but you own loading, race conditions, errors, and retries.
Security is not automatic
A Server Action executes on the server, but it is still an externally triggerable mutation boundary:
- authenticate the current user on every call;
- authorize access to the specific resource;
- validate IDs, hidden fields, and all user input;
- recalculate prices, roles, and ownership server-side;
- add rate limits and audit logs where the risk requires them;
- never return raw backend errors or stack traces to the browser.
Private backend tokens belong in Actions, Route Handlers, or server-only data modules. A direct browser request can only use intentionally public permissions or the current user's credential.
Cache revalidation
After a successful mutation, a Server Action can call Next.js revalidation APIs so affected pages or tags become fresh. A Route Handler can do the same, but you must design and secure that endpoint.
Do not invalidate before the database write succeeds. Also avoid updating only local client state while leaving a server cache stale—the old data will return after navigation or refresh.
When not to use a Server Action
- Mobile apps and third parties need the same endpoint.
- The API needs an independently versioned REST or webhook contract.
- Uploads require granular progress, chunking, or direct object-storage transfer.
- The interaction is high-frequency polling or live search.
- Next.js is only one client of an independently deployed backend.
Decision checklist
- Is this a mutation owned by the Next.js interface? Evaluate a Server Action.
- Does the browser need cancellation, progress, or continuous request control? Use fetch.
- Will multiple clients call it? Build a Route Handler or backend API.
- Does it require a private credential? Keep the call server-side.
- Does the mutation affect cached pages? Define a revalidation strategy.
Hydration warnings around interactive forms are a separate rendering problem; use the React hydration debugging guide rather than changing the request boundary blindly.
References
A migration rule of thumb
If a feature already works through an API route and is shared by multiple clients, do not move it to a Server Action just because Server Actions are newer. Keep the public contract stable and improve validation, caching, or authentication where needed.
If a form exists only inside your Next.js app and always mutates server-owned data, a Server Action can simplify the path. You avoid writing a separate HTTP handler, keep private credentials on the server, and can pair the mutation with cache revalidation in one place.
For mixed cases, split the responsibilities:
- Put reusable business logic in a server-only service module.
- Call that service from a Server Action for the web form.
- Call the same service from a Route Handler if another client needs an HTTP API.
That way the choice between Action and fetch stays a boundary decision, not a reason to duplicate the mutation.
Common smell tests
Use fetch when you are writing request orchestration: debounced search, infinite scrolling, upload progress, AbortController cancellation, polling, or a client-side SDK integration.
Use a Server Action when you are writing a user-submitted mutation: create a post, save settings, update a profile, submit a form, or trigger a server-side workflow that belongs to the current app.
Use a Route Handler when the URL itself is part of the product contract: webhook receivers, mobile clients, third-party integrations, public JSON endpoints, or anything that needs independent versioning.
Continue reading
React Hydration Failed: Causes, Debugging, and Fixes
Debug server and client rendering mismatches caused by dates, random values, browser APIs, invalid HTML, local storage, locale differences, and DOM-changing extensions.
11 min readNext.js Image Guide: sizes, fill, and Remote Images
Use the modern Next.js Image component with width and height, fill, sizes, preload, placeholders, remotePatterns, and clear rules for avoiding outdated layout props.
11 min readNext.js Routing Guide: App Router Patterns
Learn the current Next.js App Router model: static routes, dynamic segments, catch-all routes, layouts, route groups, parallel routes, and route handlers.
Subscribe to FreeMac
Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.