Skip to main content

Queries and Mutations

Queries and mutations are the public API of your application. A query is a typed read entrypoint. A mutation is a typed write entrypoint.

Why this exists

Without explicit entrypoints, every route handler becomes a public API by default. Business logic leaks into transport handlers. Access enforcement is inconsistent because each handler implements it independently.

In Gooi, domain projections and actions are private by default. An action or projection is reachable externally only when you explicitly declare it as a mutation or query. This is not a visibility flag: the absence from queries or mutations is structural unreachability, not just permission denial.

Queries

A query exposes a projection publicly. The query is the public contract; the projection is the implementation. They are deliberately separate.

queries:
- id: list_messages
access:
roles: [authenticated]
in:
q: text
sort_by: text
sort_order: text
page: int
page_size: int
defaults:
sort_by: created_at
sort_order: desc
page: 1
page_size: 20
returns:
projection: all_messages

The in block declares the typed input shape. The defaults block provides fallback values when inputs are omitted. Access is declared inline on each query because queries are called directly by surfaces (HTTP GET, CLI read commands) without going through a route.

A projection may exist without being exposed as a query. This is useful for projections used only by views. The same projection can also back multiple queries with different input shapes.

Mutations

A mutation exposes an action publicly. The mutation is the public contract; the action is the implementation.

mutations:
- id: submit_message
access:
roles: [authenticated]
in:
message: text!
run:
actionId: guestbook.submit
input:
message:
$expr:
var: input.message

The run.input block maps mutation inputs to action inputs using $expr. This mapping is explicit: the mutation decides what to pass to the action, and the action does not know it was called from a mutation.

Internal actions (like guestbook.log_rejection in the demo spec) have no corresponding mutation. They are only callable by flows and other actions. Their absence from mutations is the declaration that they are internal.

Routes

A route is a third entrypoint type that exposes a view screen for navigation. Routes are declared separately from queries and mutations.

routes:
- id: view_home
access:
roles: [authenticated]
in:
search_query: text
sort_by: text
page: int
defaults:
sort_by: created_at
page: 1
renders: home

Routes are runtime-agnostic. A web runtime navigates to a URL bound to the route. A CLI runtime renders the screen in the terminal. How inputs arrive (URL parameters, CLI arguments) is declared in wiring, not in the route definition.

Access control

Access is declared inline on each entrypoint. The access.roles field lists the roles required to call the entrypoint.

queries:
- id: list_messages
access:
roles: [authenticated]

Role definitions live in the access section at the top level of the spec. The default_policy: deny setting means any entrypoint without an explicit access clause is unreachable.

access:
default_policy: deny
roles:
authenticated:
derive:
auth_is_authenticated: []
admin:
extends: [authenticated]
derive:
auth_claim_equals: [is_admin, true]
warning

default_policy: deny is the only safe default. Any entrypoint without an explicit access clause is unreachable. This is by design.

What's next