Pages: queries and commands
A page reads with a query and writes with a command. Both name a facade — the address of a
handler, imported from @fronds/facade — and an operation of that handler.
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 `post` reads again
What the import carries
@fronds/facade is written by the scan: one const per address, carrying the handler that answers
there as a type. Compiled, post is { address: 'post' } — the handler is never imported, so no
server code reaches the bundle.
The type is the page's contract: which operations exist, what each answers, what each can refuse. You write no row shape by hand.
publish.error?.code // ErrorCode.CONFLICT | ErrorCode.FORBIDDEN | ErrorCode.NOT_FOUND
Those are the codes PostHandler.publish can throw, read from its code and the helpers it calls. A
switch over them with a never default stops compiling the day the operation learns a new refusal.
The address is the handler's name less Handler: PostHandler answers at post, CheckoutHandler
at checkout. If your page already has a variable called post, rename the import:
import { post as posts } from '@fronds/facade'.
useQuery
useQuery(facade, op, input?, opts?)
// op: an operation of the handler — anything else does not compile
// input: CallInput | () => CallInput CallInput = { params?, query?, input? }
// opts: { immediate?: boolean } false → do not call on mount
It returns, all reactive:
| Key | What it holds |
|---|---|
data | the answer as it came — one row for findById |
items | the rows of a list, whether the operation answers an array or a page |
total, hasMore | when the operation answers a page |
loading | true while the call runs |
error | the FougereError, typed by what the operation can refuse |
refresh | read again by hand — rarely needed, see below |
A function as input makes the query follow its arguments:
const { items } = await useQuery(post, 'searchByTitle',
() => ({ input: { title: submitted.value } }),
{ immediate: false });
useCommand
useCommand(facade, op)
// → { execute(input?), loading, error }
A refusal is not thrown: execute answers null and puts the FougereError in error. A page that
shows the error writes nothing else; a page that goes on checks the answer:
if (await remove.execute({ params: { id } })) navigateTo('/');
A command that succeeds makes every mounted query on the same facade read again. Publish a post, and the lists of posts on screen refresh.
What travels
The browser sends a JSON-RPC call to POST /_fougere/call — method: "post.list" — the same frame
two processes exchange. While the page renders on the server, the same call runs in memory, with no
request at all. The browser never sends the call's state: the server builds it from the request,
session included. A command is never retried on its own.
From the server: invoke
invoke calls an operation from server code — a route, a scheduled task — without reactive state:
// server/routes/feed.xml.get.ts
import Post from '@fronds/blog/entities/Post.js';
export default defineEventHandler(async () => {
const posts = await invoke<Post[]>(Post, 'list');
return renderRss(posts);
});
invoke<T>(Entity, op, input?) // by entity
invoke<T>({ address, op }, input?) // by address
- A frond in this process runs in memory; one with a
remoteinfronds:is called over the network. Your code is the same. - Inside a request, the current session goes with the call. Outside one, the state is empty.
- A refusal is the same
FougereErrora page receives.
invoke calls operations, nothing else. A service, the Logger or a value come from the constructor.