Queries & commands
A page calls a Frond with a query for reads or a command for writes. A call names a facade — an address, and the handler type that answers there. The page names it once, the way a service declares its dependency once in its constructor.
import { post } from '@fronds/facade';
const { items, total, hasMore, loading, error } = await useQuery(post, 'list');
const publish = useCommand(post, 'publish');
await publish.execute({ params: { id } });
// → every mounted query on that facade revalidates, automatically
@fronds/facade is written by the scan — one const per address, each carrying the handler
that answers there as a TYPE. Compiled, the whole of post is { address: 'post' }: the handler is
named, never imported, so no server code reaches the bundle.
What the type carries is the page's contract — which operations exist, what each one answers,
and what each one can refuse. No row shape is written by hand, which is what it is for: a
page declaring interface PostCard { … } states a shape nothing compares to the handler.
publish.error?.code // ErrorCode.CONFLICT | ErrorCode.FORBIDDEN | ErrorCode.NOT_FOUND
Those are the codes PostHandler.publish actually throws, walked from the throw sites up
through the helpers it calls. A switch over them with a never default stops compiling the
day an operation learns to refuse something else; a default: that shows the message never
does. The page chooses which of the two it wants.
A page whose rows are already called post aliases the facade on the import — import { post as postFacade } — the way any other name collision is settled.
The address is the handler's name, not an entity's — PostHandler answers at post,
CheckoutHandler at checkout. They coincide whenever a handler is named after an entity,
which is every Crud and most of the rest; a handler named after no entity — checkout —
is reached the same way.
useQuery
useQuery(facade, op, input?, opts?)
// facade: one of `@fronds/facade`' exports
// op: a method name of the handler behind it; anything else does not compile
// input: CallInput | () => CallInput CallInput = { params?, query?, input? }
// opts: { immediate?: boolean } immediate: false → don't fire on mount
Returns (all reactive), where Answered is what Handler[op] resolves to:
| Key | Type | Notes |
|---|---|---|
data | Ref<Answered | null> | the raw result (single object ops: findById, findBySlug…) |
items | ComputedRef<Row[]> | list normalization: a bare array or a ListResult both read as items |
total, hasMore | from ListResult | present when the op returns one |
loading | Ref<boolean> | |
error | Ref<FougereError | null> | the revived typed error (Errors) |
refresh | () => Promise<void> | manual revalidation — rarely needed, see the link |
A getter input makes the query reactive to its arguments:
const { items } = await useQuery(post, 'searchByTitle',
() => ({ input: { title: submitted.value } }),
{ immediate: false });
useCommand
useCommand(facade, op)
// → { execute(input?): Promise<Answered | null>, loading: Ref<boolean>, error: Ref<FougereError | null> }
A refusal is state, not an exception: execute answers null and puts the FougereError in
error, so a page that shows the error writes nothing else, and a page that goes on checks the
answer — if (await remove.execute({ params: { id } })) navigateTo('/').
The link
A successful command on a facade revalidates active queries on that same facade. After a post is published, post lists are therefore refreshed. Clearing this cache does not change operation results, but causes new reads.
The wire — and SSR
The browser POSTs the same JSON-RPC frame process-to-process transport uses
(POST /_fougere/call, method: "post.list", params = the invocation).
The documentation calls the
POST /_fougere/callendpoint used byuseQuery,useCommand, andinvokethe envelope. The REST and GraphQL surfaces translate their protocols to the same operations.
Two properties:
- During SSR, the same call collapses to in-process execution — no network, no port.
- The browser does not provide
state: the server builds it from the request context, including the session.
The client transport does not automatically retry commands and preserves typed errors (Errors).
Next: Forms — state, validation, and submission.