Episode 2 — React Frontend Architecture NextJS / 2.21 — Working with Server Actions
2.21 — Interview Questions: Server Actions
Maps to 2.21.a (vs Route Handlers), 2.21.b (
'use server'boundaries), 2.21.c (forms, hooks, revalidation), 2.21.d (security, errors, production).
Beginner
Q1. What is a Server Action in Next.js?
Model answer: A Server Action is an async function that runs only on the server and is marked with the 'use server' directive. The framework lets you attach it to forms or call it from eligible client code through a generated proxy, so you can perform mutations — database writes, calling payment APIs, sending email — without writing a manual fetch wrapper for every form in your own app.
It is not a replacement for all HTTP APIs: it is optimized for same-application mutations where Next.js controls both the UI and the server runtime.
Q2. How is that different from an API route?
Model answer: A Route Handler is an explicit HTTP endpoint at a path like /api/users. Any client that speaks HTTP can call it — mobile apps, webhooks, partner integrations. You choose verbs, status codes, headers, and response bodies.
A Server Action is more like a remote function invoked through Next’s action protocol. The contract is your function signature and serializable arguments, not a documented REST resource. I use Actions for in-app forms and mutations; I use Route Handlers when I need a public or versioned HTTP surface.
Q3. What does 'use server' do?
Model answer: It is a string directive, similar in spirit to 'use client', that tells the bundler and runtime: this module (or function) is part of the server graph. Server-only dependencies like database drivers and secret environment variables stay off the client bundle. When client code imports an action, it receives a callable stub that forwards to the server implementation.
Q4. Can you put secrets inside a Server Action?
Model answer: You can read server-only env vars there, yes—but never return secrets to the client in the action result or thrown errors. Treat the return payload like any other public channel unless you have a tightly controlled internal UI.
Intermediate
Q5. How would you return validation errors from a form action?
Model answer: For expected validation failures I prefer returning structured data — for example { ok: false, fieldErrors: { email: 'Invalid' } } — instead of throwing, because throwing is better reserved for exceptional cases.
On the client I wire the action through useActionState so React passes the previous state into the action and merges the return value into UI state. That gives me inline errors and pending states without a separate state machine for simple forms.
Q6. How do you pass a resource id into a Server Action securely?
Model answer: HTML forms only post field values. I can use bind to prepend trusted arguments like updatePost.bind(null, postId) so the signature becomes (formData) => void on the wire while my server function still receives (postId, formData).
Critical point: I never trust ids alone — I load the row on the server and verify the current session owns or may edit that resource before performing the update.
Q7. After a mutation, why might the UI still show stale data? How do you fix it?
Model answer: Next.js caches Server Component payloads. After a mutation I call revalidatePath for affected routes or revalidateTag for data tagged in fetch / unstable_cache. Without that, the next navigation might show cached RSC output until TTL or manual invalidation.
Q8. What is the difference between useFormStatus and useActionState?
Model answer: useActionState pairs a form with an action that receives previous state and returns the next state (great for validation messages). useFormStatus reads pending/submitting state for a child of the <form>—ideal for disabling buttons and spinners without threading props manually.
Q9. How do you call a Server Action from a non-form interaction?
Model answer: Wrap the call in startTransition from a Client Component: startTransition(() => deleteTodo(id)). Still enforce authz on the server exactly like a Route Handler.
Advanced
Q10. Are Server Actions “secure by default”?
Model answer: No endpoint that mutates data is secure without application-level checks. The action is still reachable with crafted requests. I always re-run authentication and authorization inside the action, validate with a schema like Zod, and apply rate limiting for sensitive flows. I also avoid leaking internal error details to clients.
For CSRF, I follow the framework’s current guidance for my Next version — many setups include same-site header checks, but I still treat high-risk operations with extra confirmation or step-up auth rather than relying on any single mechanism.
Q11. When would you refuse to use a Server Action?
Model answer: When I need a stable HTTP contract for external systems — Stripe webhooks, mobile clients, third-party API consumers — I use Route Handlers (or a separate backend). I also prefer Route Handlers when I need very explicit control over streaming responses, unusual HTTP semantics, or public API documentation (OpenAPI).
Q12. How do you test Server Actions?
Model answer: Because they are plain async functions, I import them in Vitest/Jest and call them with synthetic FormData or arguments. I mock the service layer that talks to the database. For broader confidence I add integration tests around the user-visible behavior depending on what our stack supports for Next.js. The key is separating thin actions from domain services so tests stay fast and deterministic.
Q13. How do you handle production errors from actions without leaking internals?
Model answer: Return typed failure objects for expected cases ({ ok: false, code: 'CARD_DECLINED' }). For unexpected failures, log server-side with stack + request id, and return a generic user message. Never surface raw DB errors or file paths to clients.
Q14. What is your rate limiting strategy for sensitive Server Actions?
Model answer: Per-IP or per-user token buckets backed by Redis or an edge limiter—not in-memory singletons on serverless. Combine with CAPTCHA or step-up auth for abuse-prone endpoints (signup, password reset).
Quick-Fire Table
| # | Question | One-Line Answer |
|---|---|---|
| 1 | What directive marks server-only actions? | 'use server' |
| 2 | Primary transport for simple form mutations? | HTML form POST wired to action={serverFn} |
| 3 | Hook for stateful form actions + errors? | useActionState |
| 4 | Hook for submit pending in a child button? | useFormStatus |
| 5 | Function to refresh cached RSC data for a path? | revalidatePath |
| 6 | Way to prepend args to an action used in a form? | .bind(null, arg) |
| 7 | Why validate on the server if the client already validates? | Client validation is UX-only; attackers bypass it |
| 8 | Best tool for Stripe webhooks in Next? | Route Handler with signature verification |
| 9 | Non-form action invocation? | startTransition(() => action(arg)) |
| 10 | CSRF posture starting point? | Follow framework guidance + same-site cookies + risky flows double-checked |
Answers at a glance (2.21.a → 2.21.d)
| Lesson | One sentence |
|---|---|
| 2.21.a — vs API routes | Actions optimize same-app mutations; Route Handlers publish HTTP for any client. |
2.21.b — use server | Marks server graph; serializable args cross the wire; secrets never echoed back. |
| 2.21.c — Forms | useActionState + useFormStatus + revalidatePath/Tag + redirect compose the UX story. |
| 2.21.d — Security/production | Treat actions like POST handlers: authn, authz, Zod, rate limits, safe errors, observability. |