You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Epic. Follow-up to #472 (SSR action-result seeding). Give server actions explicit HTTP-verb semantics with great DX for humans and AI agents, with cacheable GET reads and tag-based invalidation, grounded in a comparison against Next (POST-only, reads are Server Components), tRPC (query/mutation), and TanStack Start (createServerFn({ method: 'GET' | 'POST' })).
Problem
The auto-generated RPC stub for a server action is always POST, so the read path has no HTTP-level caching: a client-initiated read (a refetch on a prop change, a second component, a soft nav) always hits the server. webjs has no RSC server/client split, so reads and writes BOTH flow through the same action mechanism (async render() calls an action for reads, events/forms call one for writes). Unlike Next, the read/write distinction cannot live at the component-type level, so it must live on the action. This is the gap tRPC (query/mutation) and TanStack Start (method) both fill on their RPC layer, and the one webjs should fill too.
Design (decided)
Keep the 'use server' directive and one-function-per-file. The function stays a PLAIN export async function; per-verb behavior comes from RESERVED sibling config exports the framework reads statically, exactly like a page declares export const revalidate / metadata next to export default function Page. No folder automation. Additive and non-breaking: an action with no config exports stays a POST, exactly as today.
Reserved config names (special only in a 'use server' file): method, cache, tags, invalidates, validate. The call site is unchanged: a component does await getUser(7) (a cacheable GET stub on the client, a direct in-process call on the server); the config is read statically by the stub generator (no build step).
Full verb matrix (decided)
The complete GET / POST / PUT / PATCH / DELETE set is supported (not just GET/POST). Per-verb defaults:
Verb
Transport
Cacheable
Idempotent
CSRF
Method enforced
GET
args in URL (stable key, 1MB cap -> POST fallback)
yes (opt-in cache)
yes (safe)
exempt
yes
POST
rich body
no
no
required
yes
PUT
rich body
no
yes
required
yes
PATCH
rich body
no
no
required
yes
DELETE
args in URL
no
yes
required
yes
Caching DX (webjs is ahead of TanStack here: TanStack has no caching on server fns)
A GET action emits Cache-Control + an ETag, reusing the conditional-GET funnel (#240) and the server cache() + revalidateTag (#242). A mutation's invalidates tags evict the matching server cache entries AND the matching client cache entries, so the next read refetches fresh. Keep caching at the HTTP layer plus the server cache(); do NOT add a bespoke client query-store (off-thesis).
Improvements adopted from the TanStack research
405 + Allow on a method mismatch, before parsing the payload (TanStack server-functions-handler.ts).
GET payload cap (1MB) with documented POST fallback (TanStack MAX_PAYLOAD_SIZE).
Compile-time serializable input/output typing: a non-serializable arg or return (a function, a class instance) is a type error, not a runtime surprise (TanStack ValidateSerializableInput/Output).
An action may return a raw Response (already true for page-actions; extend to the RPC path).
Remove expose; REST via route.ts
route.ts already is the framework's first-class HTTP surface (file-routed, named exports per verb, middleware), so expose is removed. A public REST endpoint is a route.ts that imports and calls the action. Provide an optional action.route adapter (a Request -> Response handler reading the file config) and action.validate(input) for the common case; the explicit handler is the always-works baseline. validate is a BOUNDARY concern: it runs at the RPC endpoint and at any route.ts boundary, NOT on a direct server-to-server in-process call.
AI-agent + human DX
The verbs map onto REST priors every model and human already know.
Editor intelligence (@webjsdev/intellisense): hover shows the verb + cache; go-to-definition jumps to the server fn.
A generated typed action surface (like webjs types emits the Route union) is a stretch goal.
webjs check enforces the contract (the --json agent loop): a GET whose handler writes, a PUT/DELETE that is not idempotent, an invalidates tag no GET produces (a typo), args too large/rich for a GET URL, a configured file with more than one callable function, and a reserved-name collision. Each violation ships a fix string.
Sub-issues (the epic)
Streaming RPC results (return a ReadableStream / AsyncIterable).
Full verb matrix: GET/DELETE encode args in the URL with a stable canonical key (1MB cap -> POST fallback + dev warning); POST/PUT/PATCH send the rich body; method absent => POST (non-breaking).
405 + Allow on a method mismatch, before parsing the payload.
GET responses carry Cache-Control + ETag; a repeat call within the window is served from the browser cache (network probe); a stale call revalidates via 304.
invalidates evicts the matching server cache() entries (via revalidateTag) AND the client cache entries.
expose removed; REST is a route.ts calling the action; action.route / action.validate helpers provided; validate is boundary-only (not on direct server-to-server calls).
End-to-end types preserved; non-serializable input/output is a compile-time error; an action may return a raw Response.
webjs check rules added (one-fn-per-config-file, reserved-name collision, GET-writes, idempotency, invalidates-tag typo, GET-args-too-large), each with a fix string.
Epic. Follow-up to #472 (SSR action-result seeding). Give server actions explicit HTTP-verb semantics with great DX for humans and AI agents, with cacheable GET reads and tag-based invalidation, grounded in a comparison against Next (POST-only, reads are Server Components), tRPC (query/mutation), and TanStack Start (
createServerFn({ method: 'GET' | 'POST' })).Problem
The auto-generated RPC stub for a server action is always POST, so the read path has no HTTP-level caching: a client-initiated read (a refetch on a prop change, a second component, a soft nav) always hits the server. webjs has no RSC server/client split, so reads and writes BOTH flow through the same action mechanism (
async render()calls an action for reads, events/forms call one for writes). Unlike Next, the read/write distinction cannot live at the component-type level, so it must live on the action. This is the gap tRPC (query/mutation) and TanStack Start (method) both fill on their RPC layer, and the one webjs should fill too.Design (decided)
Keep the
'use server'directive and one-function-per-file. The function stays a PLAINexport async function; per-verb behavior comes from RESERVED sibling config exports the framework reads statically, exactly like a page declaresexport const revalidate/metadatanext toexport default function Page. No folder automation. Additive and non-breaking: an action with no config exports stays a POST, exactly as today.Reserved config names (special only in a
'use server'file):method,cache,tags,invalidates,validate. The call site is unchanged: a component doesawait getUser(7)(a cacheable GET stub on the client, a direct in-process call on the server); the config is read statically by the stub generator (no build step).Full verb matrix (decided)
The complete
GET / POST / PUT / PATCH / DELETEset is supported (not just GET/POST). Per-verb defaults:cache)Caching DX (webjs is ahead of TanStack here: TanStack has no caching on server fns)
A GET action emits
Cache-Control+ an ETag, reusing the conditional-GET funnel (#240) and the servercache()+revalidateTag(#242). A mutation'sinvalidatestags evict the matching server cache entries AND the matching client cache entries, so the next read refetches fresh. Keep caching at the HTTP layer plus the servercache(); do NOT add a bespoke client query-store (off-thesis).Improvements adopted from the TanStack research
Allowon a method mismatch, before parsing the payload (TanStackserver-functions-handler.ts).MAX_PAYLOAD_SIZE).ValidateSerializableInput/Output).Response(already true for page-actions; extend to the RPC path).Remove
expose; REST viaroute.tsroute.tsalready is the framework's first-class HTTP surface (file-routed, named exports per verb, middleware), soexposeis removed. A public REST endpoint is aroute.tsthat imports and calls the action. Provide an optionalaction.routeadapter (a Request -> Response handler reading the file config) andaction.validate(input)for the common case; the explicit handler is the always-works baseline.validateis a BOUNDARY concern: it runs at the RPC endpoint and at anyroute.tsboundary, NOT on a direct server-to-server in-process call.AI-agent + human DX
@webjsdev/intellisense): hover shows the verb + cache; go-to-definition jumps to the server fn.list_actions(Add a read-only webjs MCP server and webjs check --json #262) reports verb + cache + tags + invalidates so an agent sees the full data contract.webjs typesemits theRouteunion) is a stretch goal.webjs checkenforces the contract (the--jsonagent loop): aGETwhose handler writes, aPUT/DELETEthat is not idempotent, aninvalidatestag noGETproduces (a typo), args too large/rich for a GET URL, a configured file with more than one callable function, and a reserved-name collision. Each violation ships a fix string.Sub-issues (the epic)
ReadableStream/AsyncIterable).Acceptance criteria (core)
method/cache/tags/invalidates/validate) read statically;'use server'retained; one-function-per-file enforced for configured files.Allowon a method mismatch, before parsing the payload.Cache-Control+ ETag; a repeat call within the window is served from the browser cache (network probe); a stale call revalidates via 304.invalidatesevicts the matching servercache()entries (viarevalidateTag) AND the client cache entries.exposeremoved; REST is aroute.tscalling the action;action.route/action.validatehelpers provided;validateis boundary-only (not on direct server-to-server calls).Response.webjs checkrules added (one-fn-per-config-file, reserved-name collision, GET-writes, idempotency, invalidates-tag typo, GET-args-too-large), each with a fix string.list_actionsreports verb + cache + tags + invalidates.expose.Composes with and follows up #472.