Episode 2 — React Frontend Architecture NextJS / 2.21 — Working with Server Actions

2.21 — Exercise Questions: Server Actions

Hands-on prompts. Implement in a Next.js App Router project (or pseudo-code on paper first, then code).


<< 2.21 Overview


A. Foundations (2.21.b)

  1. Create app/hello/actions.ts with 'use server' and export sayHello(formData: FormData) that reads a name field and returns a greeting string. Render the result in a Server Component page without using 'use client'.

  2. List three things you must not do inside a Server Action body.

  3. Explain out loud: why can a Client Component import deleteTodo from a 'use server' file without bundling your ORM into the browser?


B. Forms & State (2.21.c)

  1. Build a signup form using useActionState that shows field-level errors for invalid email and short password.

  2. Extract a SubmitButton that uses useFormStatus to disable itself while the form is submitting.

  3. Add a server-side Zod schema to the same form. Map Zod’s flatten().fieldErrors into your UI.


C. Data & Cache (2.21.a + 2.21.c)

  1. After creating a blog comment via a Server Action, call revalidatePath so the comment list on the post page updates. Verify stale-then-fresh behavior manually.

  2. Replace path revalidation with revalidateTag on post data fetched with { next: { tags: ['post-' + slug] } }.


D. Authorization & Safety (2.21.d)

  1. Implement deletePost(postId: string) that loads the post, compares authorId to the session user, and returns { ok: false, code: 'FORBIDDEN' } instead of throwing when unauthorized.

  2. Add naive per-IP rate limiting in memory (acceptable only for demos) — articulate why production would use Redis or an edge limiter instead.


E. Comparison with Route Handlers (2.21.a)

  1. Implement the same “create comment” flow twice: once with a Server Action, once with POST in route.ts. Document which callers each supports.

  2. Write a curl command that successfully hits your Route Handler. Explain why curl cannot “call the Server Action” the same way without replicating framework internals.


F. Stretch

  1. Use useTransition to call a Server Action from a button that is not inside a <form>.

  2. Debate (written paragraph): in a large team, when would you forbid Server Actions for certain domains (billing, admin) and require Route Handlers + explicit OpenAPI instead?


Answers & guidance (self-check)

1. sayHello without client

  • Server page calls the action from a <form action={sayHello}>; display message using useActionState return or progressive enhancement pattern per your chosen UX (pure server redirect vs stateful client wrapper).

2. Three must-nots (examples)

  • Do not trust client-only validation; do not return secrets; do not perform unbounded slow work without timeouts; do not skip authz checks; do not leak stack traces (pick three when answering aloud).

3. Why ORM not in browser

  • The client imports a stub; the bundler strips server graph from client bundle. Actual execution happens on the server runtime.

4–6. Forms

  • useActionState signature: action receives (prevState, formData) after wrapping; return discriminated union for { ok: true } vs { ok: false, fieldErrors }.
  • useFormStatus must be used in a child of <form> to observe pending.
  • Map Zod error.flatten().fieldErrors keys to inputs.

7–8. Revalidation

  • revalidatePath('/posts/[slug]') (or concrete path) after mutation.
  • Tags: ensure fetch in RSC uses same tag string; webhook or action calls revalidateTag('post-' + slug).

9. Authz pattern

  • const post = await db.post.findUnique(...); if !postNOT_FOUND; if post.authorId !== session.user.id → structured FORBIDDEN (not thrown unless you want error boundary semantics).

10. In-memory limiter

  • Serverless has multiple instances—each has its own memory, so limits are ineffective globally. Use Redis/edge rate limiter for shared counters + TTL.

11. Dual implementation

  • Table: Server Action supports Next app internal forms/transitions; Route Handler supports curl/mobile/partner HTTP.

12. curl vs actions

  • Actions use Next-internal POST protocol with encoded fields—not your arbitrary JSON REST unless you build it. curl can hit /api/... easily.

13. useTransition

  • Client button onClick={() => startTransition(() => deleteTodo(id)))}; still show optimistic UI carefully.

14. Forbid actions in billing/admin

  • Reasons: need auditable HTTP API, strict versioning, penetration testing against known routes, consumers outside React, compliance reviews, long-lived integrations—OpenAPI + Route Handlers reduce ambiguity.

<< 2.21 Overview