3. Server Actions - A Detailed Guide
A document explaining Server Functions / Server Actions - calling a server function from a form or client without writing /api/... for every mutation.
On this page
- Table of Contents
- 1. The Problem Server Actions Solve
- 2. What Is a Server Action - the Mental Model
- 3. Declaring "use server"
- 4. Calling from a Form - Progressive Enhancement
- 5. Calling from a Client Component
- 6. Server Action vs Route Handler
- 7. After Mutating: Revalidate & Refresh
- 8. Security - Treat It as a Public API
- 9. Nova's Flow: Adding to Cart
- 10. FAQ & File Map
3. Server Actions - A Detailed Guide
A document explaining Server Functions / Server Actions - calling a server function from a form or client without having to write /api/... for every mutation.
Read in order: Section 1 → 2 (declaration) → 3–4 (form vs client) → 5 (revalidate) → 6 (Nova flow) → 7 (FAQ).
Docs: Mutating Data · serverActions config
Table of Contents
- The problem Server Actions solve
- What is a Server Action - the mental model
- Declaring
"use server" - Calling from a Form - progressive enhancement
- Calling from a Client Component
- Server Action vs Route Handler
- After mutating: revalidate & refresh
- Security - treat it as a public API
- Nova's flow: adding to cart
- FAQ & file map
1. The Problem Server Actions Solve
The old way (SPA / Pages API):
Client onClick → fetch POST /api/cart/add → JSON → setState
Requires: writing a Route Handler, validation, CORS, duplicated types.
Server Actions:
Client onClick → await addToCart() // a server function, Next.js handles the transport
→ Nova: app/lib/services/cart.ts - addToCart, updateCartItem, …
2. What Is a Server Action - the Mental Model
| Characteristic | Details |
|---|---|
| HTTP method | POST only (a Next.js convention) |
| Endpoint | Auto-generated, hard-to-guess ID (v15+) |
| Where it runs | Always on the server |
| Return value | Must be serializable |
3. Declaring "use server"
For an entire file
// app/lib/services/cart.ts
"use server";
export async function addToCart(productId: string, quantity: number) {
// ...
}
For a single function inside a client file (rarely used)
"use client";
async function submit() {
"use server";
// ...
}
Nova uses whole-file declarations in services/cart.ts, actions.ts.
4. Calling from a Form - Progressive Enhancement
// login-form.tsx
import { authenticate } from "@/app/lib/actions";
<form action={authenticate}>
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">Sign in</button>
</form>
// actions.ts
"use server";
export async function authenticate(prevState: string | undefined, formData: FormData) {
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirectTo: "/products",
});
}
useActionState (React 19): shows errors + isPending:
const [state, formAction, isPending] = useActionState(authenticate, undefined);
Note: a successful signIn usually throws a redirect - catch AuthError, and rethrow the redirect.
5. Calling from a Client Component
"use client";
import { addToCart } from "@/app/lib/services/cart";
import { syncCartBadge } from "@/app/lib/cart-events";
startTransition(async () => {
const summary = await addToCart(productId, qty, color, storage);
syncCartBadge(summary.totalItems);
});
| Step | What happens |
|---|---|
| 1 | The client serializes the arguments |
| 2 | POSTs to the Next.js action endpoint |
| 3 | The server runs addToCart |
| 4 | The CartSummary is returned to the client |
Don't use an action for heavy, repeated reads (e.g., the navbar calling getCart on every mount) - fetch it in a server layout instead.
6. Server Action vs Route Handler
| Criteria | Server Action | Route Handler route.ts |
|---|---|---|
| Fixed URL | No (internal endpoint) | /api/checkout |
| Caller | Form, await action() | fetch, Stripe, Postman |
| Stripe webhook | ❌ | ✅ |
| Public mobile app REST | Hard | ✅ |
| Type-safe from within the app | ✅ great | Moderate |
How Nova splits things up:
| Task | Technology |
|---|---|
| Cart CRUD | Server Actions in cart.ts |
| Stripe checkout URL | POST /api/checkout |
| Stripe webhook | POST /api/stripe/webhook |
7. After Mutating: Revalidate & Refresh
Full details: 5. Data Fetching and Cache.
Summary of Nova's approach - revalidateAfterCartChange():
revalidateTag("products"); // Data Cache catalog
revalidatePath("/cart"); // RSC route
revalidatePath("/products", "layout");
refresh(); // Currently viewed route
| API | When |
|---|---|
revalidateTag | For fetches with a tag (ISR catalog) |
revalidatePath | Forces a page/layout to re-render |
refresh() | Soft-refreshes the currently viewed route |
Manually calling refresh from the client:
// actions.ts
export async function refreshShopData() {
refreshShopRoute();
}
8. Security - Treat It as a Public API
Next.js 15: unguessable Action IDs, dead code elimination - but it's still a public HTTP POST.
Required in every action:
authFetch/ check the session.- Validate the input (Zod).
- Never trust a
userIdfrom the client if it can be obtained from the cookie/server instead.
// ❌ Trusting the client body
await fetch(`/cart?userId=${body.userId}`);
// ✅ Server-side cookie / token
const userId = cookies().get("user_id")?.value;
Production: set serverActions.allowedOrigins in next.config.ts if you're behind a reverse proxy.
9. Nova's Flow: Adding to Cart
Files: productForm.tsx → services/cart.ts → revalidate-shop.ts
10. FAQ & File Map
FAQ
Q: Do actions run on the Edge?
Depends on the route's runtime; Stripe/cart logic uses Node APIs → usually the Node runtime.
Q: What about redirect() inside an action?
It's an internal throw - don't swallow it in a catch block.
Q: When should you NOT use an Action?
Webhooks, public REST, streaming upload progress → use a Route Handler instead.
Exercises
- Why doesn't the Stripe webhook use a Server Action? → It needs a fixed URL, the raw body, and is called by an external caller.
- After
addToCart, why call bothrevalidateTagandrefresh()? → See file 5, section 8.
File Map
app/lib/services/cart.ts → mutations + getCartSummary
app/lib/actions.ts → authenticate, refreshShopData
app/lib/revalidate-shop.ts → revalidate helpers
app/ui/.../productForm.tsx → calls addToCart
Next: 4. Route Handlers · 5. Data Fetching
