Skip to content

scaffold: server-actions card should teach HTTP verbs and GET caching #1151

Description

@vivek7405

Problem

The full-stack scaffold gallery (packages/cli/templates/gallery/) never teaches
HTTP-verb server actions (#488). A freshly scaffolded app browsing
/features/server-actions learns the 'use server' vs server-only-.server.ts
boundary, export const middleware, actionContext(), and actionSignal(), but
nothing about the reserved sibling config exports that decide an action's
transport and caching: method, cache, tags, invalidates.

What the gallery ships today:

  • export const method = 'GET' appears twice, as incidental usage with a short
    comment, in modules/todo/queries/list-todos.server.ts:9 and
    modules/async-render/queries/server-greeting.server.ts:5. Neither is on a card
    that explains the idiom, so an agent reading the gallery for the pattern finds it
    only by accident.
  • export const cache, tags, and invalidates appear NOWHERE as working code in
    the UI gallery. The caching card (app/features/caching/page.ts:36) namechecks
    "or a GET action's export const cache" in a trailing sentence and demonstrates
    nothing; that card is entirely about the page-level export const revalidate +
    revalidatePath path.
  • The api template does teach it (packages/cli/lib/create.js:946-975:
    method = 'GET' + cache = 30 + tags, paired with invalidates on the
    mutation), so the full-stack app, the one most agents scaffold, is the surface
    missing it.

#576 adopted the verb idiom across the monorepo apps and the templates' modules/users
code, but the gallery card layer was not extended with it, so the teaching gap
survived the adoption.

Design / approach

Extend the EXISTING server-actions card rather than adding a new one. Verbs are a
property of a server action, not a separate feature, and the card is already the
place where the action's config exports (middleware) are explained, so this is a
natural second section on the same card. Adding a /features/http-verbs card would
split one concept across two routes and grow the prune surface for an app that does
not need it.

The card should teach, with runnable code the visitor can trigger:

  • export const method = 'GET' makes args ride the URL, is CSRF-exempt, carries
    Cache-Control + a weak ETag (304 on If-None-Match), and is SSR-seeded so the
    component does not re-fetch on hydration. Absent method means POST.
  • export const cache = N (or { maxAge, swr }) is the response cache window,
    private by default. Call out the safety rule explicitly: { public: true }
    shares the response across users keyed only by URL + args, so it is only for data
    identical for every visitor, the same rule as a page's export const revalidate.
  • export const tags labels a GET's cached entry; export const invalidates on the
    mutation evicts those tags on success, so the next read refetches instead of
    serving a stale browser-cached value (X-Webjs-Invalidate).

A good demo shape: a cached GET read whose value visibly freezes for its window
(a timestamp or counter), plus a mutation button declaring invalidates for the
same tag, so clicking it makes the next read change immediately. That is the same
before/after affordance the caching card already uses with revalidatePath, which
keeps the two cards consistent and lets the caching card's dangling sentence link
here instead of trailing off.

Implementation notes (for the implementing agent)

Where to edit:

  • packages/cli/templates/gallery/app/features/server-actions/page.ts: the card
    copy. It currently covers only the middleware / actionContext / actionSignal
    story (added by Scaffold gallery: add a <webjs-frame> demo and rework the server-actions demo to show middleware/actionContext/actionSignal #988). Add the verbs + caching section and mount the new demo
    component.
  • packages/cli/templates/gallery/modules/server-actions/: the demo code lives
    here, alongside actions/greet.server.ts, middleware/require-auth.server.ts,
    utils/format.server.ts, components/greeter.ts.
    One function per configured file is a hard rule: a file carrying method /
    cache / tags / invalidates / validate / middleware with more than one
    callable export is a webjs check error. So the cached GET goes in its own
    queries/<name>.server.ts and the invalidating mutation in its own
    actions/<name>.server.ts. greet.server.ts already models the pattern of
    keeping a private non-exported helper in the file to stay within the rule.
  • packages/cli/templates/gallery/modules/gallery/nav.ts:30: the card blurb still
    reads "A use-server RPC action next to a server-only .server.ts utility, and why
    the boundary matters." Update it to mention verbs / caching, or the index will
    under-sell what the card now teaches.
  • packages/cli/templates/gallery/app/features/caching/page.ts:33-38: replace the
    dangling "or a GET action's export const cache" sentence with a real link to
    /features/server-actions, so the two cards cross-reference instead of leaving a
    dead end.

Landmines / gotchas:

  • The card must survive gallery:clear pruning as a unit. The server-actions
    card already depends on the auth card (the greeter 401s when signed out, and the
    page says "prune both together"). Do not add a third cross-card dependency; keep
    the new demo self-contained inside modules/server-actions/.
  • cache is private by default and the demo must stay that way. Do not write
    { public: true } in scaffold code an agent will copy: the gallery is a teaching
    surface and a copied public: true on a per-user read is a real cross-user data
    leak. Teach the flag by naming it and the rule, not by shipping it.
  • A streamed result is never cached / ETagged / seeded, so do not combine the
    new cached-GET demo with the streaming-action demo.
  • A GET's args ride the URL with a 4KB cap (over that it falls back to POST);
    keep the demo's args small so the demonstrated transport is the one described.
  • SSR seeding (feat: seed SSR action results into hydration so async render does not re-fetch (follow-up to #469) #472) hides the first RPC. A component calling the cached GET
    during SSR consumes the seed on its first client call, so a naive "watch the
    network tab" instruction is wrong for the first paint. Either say so in the copy
    or drive the demo from an explicit user-triggered refetch.
  • Prose in scaffold templates is subject to the repo's banned-glyph rules (no
    em-dashes, no space-surrounded hyphen or semicolon as a pause, WebJs capitalized
    in prose): .claude/hooks/block-prose-punctuation.sh blocks a violating edit.

Invariants to respect:

  • Server-only code stays in .server.{js,ts}; the demo component imports the action
    through the normal import (the RPC stub), never a hand-written fetch()
    (AGENTS.md invariant 1 + the server-actions section).
  • Light-DOM components use Tailwind / the #components/ui/* class helpers
    (cardClass(), buttonClass()), matching the rest of the gallery after Refactor the scaffold gallery onto a @webjsdev/ui class-helper design system #1059.
  • Gallery pages are pages, so they never hydrate: all interactivity belongs in the
    demo component under modules/server-actions/components/.

Tests + docs surfaces:

  • test/scaffolds/scaffold-gallery.test.js asserts every app/features/<name> page
    exists, is listed in the nav, and carries no placeholder marker. A new module file
    under an already-listed route needs no new entry, but if the demo adds a route,
    update FEATURES (L29) and MODULE_ROUTES (L39).
  • test/scaffolds/gallery-coverage.json gates @webjsdev/core / @webjsdev/server
    EXPORTS and routing conventions. Reserved action config exports (method, cache,
    tags, invalidates) are not module exports, so nothing in the manifest would
    have caught this gap. Consider whether the manifest (or a sibling assertion in
    scaffold-gallery.test.js) should assert the gallery demonstrates each reserved
    config export, which is what would keep this from regressing.
  • Verify by generating an app and booting it (webjs create + webjs dev +
    webjs check), per the webjs-scaffold-sync skill: the generators emit strings, so
    an escaping bug only shows in a freshly generated app.
  • Doc surfaces: the scaffold-sync skill's map, plus anywhere the gallery's card list
    is enumerated. The docs site already covers verbs (docs: server-actions page missing HTTP-verb surface (#488-#492) #572) and the swr option
    (docs: explain the swr cache option on GET actions; README omits cached GET reads #1148), so this is scaffold-side only unless the card introduces new phrasing
    worth mirroring.

Acceptance criteria

  • /features/server-actions in a freshly generated full-stack app teaches
    export const method, cache, tags, and invalidates, with the
    private-by-default safety rule stated
  • The card ships a runnable demo where a cached GET visibly freezes for its
    window and an invalidates mutation makes the next read change immediately
  • Each configured action file holds exactly one callable function, and
    webjs check passes on the generated app
  • The gallery nav blurb (modules/gallery/nav.ts:30) reflects the expanded card
  • The caching card links to the server-actions card instead of dangling on
    "or a GET action's export const cache"
  • Generate + boot + webjs check verified on a fresh app, not just in-repo
  • Gallery tests cover the new files; a counterfactual proves the assertion fires
  • Consider a regression guard so a future reserved config export cannot go
    undemonstrated silently

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions