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:

KeyTypeNotes
dataRef<Answered | null>the raw result (single object ops: findById, findBySlug…)
itemsComputedRef<Row[]>list normalization: a bare array or a ListResult both read as items
total, hasMorefrom ListResultpresent when the op returns one
loadingRef<boolean>
errorRef<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('/').

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/call endpoint used by useQuery, useCommand, and invoke the 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.

Built with Fougere and Nuxt, hosted on GitHub Pages.