---
title: '3. Queries & data binding'
description: Declare typed reads once; get dependency stamps, data bindings, and shared client data derived.
order: 3
---

# Queries & data binding

So far the catalog is hardcoded. In this chapter you add real data: a product list and a cart
badge. You declare two queries, and every downstream surface — dependency stamps, binding paths,
the JSON the page ships — comes from them. Step state: `site/tutorial/steps/03-queries/`.

## Declare domains

Domains are named groups of data that writes touch and reads depend on. They are the currency
the invalidation graph trades in, so they come first:

```ts
export const cart = domain();
export const product = domain();
```

> **The production path.** The tutorial uses a plain in-memory store so every moving part stays
> visible. With [`@kovojs/drizzle`](/guides/data-layer/), this is derived from real tables instead
> of declared by hand — here, domains come from schema annotations and read sets are extracted from
> the query ASTs (the JOIN is the declaration). The [data-layer guide](/guides/data-layer/) is the
> home for that story.

The tutorial keeps a plain store:

```ts
export type ShopProduct = {
  id: string;
  name: string;
  stock: number;
  unitPrice: number;
};

export type CartItem = {
  productId: string;
  qty: number;
  unitPrice: number;
};

export interface ShopDb {
  cartItems: CartItem[];
  products: Map<string, ShopProduct>;
}

export interface ShopRequest {
  db: ShopDb;
}

export function createShopDb(): ShopDb {
  return {
    cartItems: [],
    products: new Map([
      ['p1', { id: 'p1', name: 'Pour-over kettle', stock: 5, unitPrice: 1499 }],
      ['p2', { id: 'p2', name: 'Ceramic dripper', stock: 2, unitPrice: 2599 }],
      ['p3', { id: 'p3', name: 'Paper filters', stock: 8, unitPrice: 399 }],
    ]),
  };
}
```

## Declare the reads once

A query couples an explicit access decision, a loader, and the domains it reads. This tutorial's
catalog and single-cart fixture are intentionally public; `publicAccess(...)` records that review
decision instead of relying on an implicit allow. The read set is the entire invalidation
registration: no query subscribes to mutations, and no mutation enumerates queries.

> **Where the primitives live.** `query`, `route`, `mutation`, `s`, `domain`, `guards`, and
> `session` import from `@kovojs/server`; `component` and `form` import from `@kovojs/core`. Server
> facts in `@kovojs/server`, the component/form model in `@kovojs/core` — that's the whole split.

```ts
export function loadCart(db: ShopDb): CartResult {
  return { count: db.cartItems.reduce((total, item) => total + item.qty, 0) };
}

export function loadProducts(db: ShopDb): ProductsResult {
  return {
    items: [...db.products.values()].sort((left, right) => left.id.localeCompare(right.id)),
  };
}
```

```ts
export const cartQuery = query({
  access: publicAccess('tutorial single-cart storefront read'),
  load: (_input: unknown, context?: ShopQueryLoadContext) => loadCart(dbFrom(context)),
  reads: [cart],
});

export const productsQuery = query({
  access: publicAccess('tutorial public product catalog'),
  load: (_input: unknown, context?: ShopQueryLoadContext) => loadProducts(dbFrom(context)),
  reads: [product],
});
```

A query's relationship to future writes is fixed by what it reads, not by anything you remember
to wire up, so you can't forget a dependency here. Chapter 5 cashes this in.

## Bind queries from components

The cart badge consumes the cart query. Your TSX says only that:

```tsx
export const CartBadge = component({
  queries: { cart: cartQuery },
  render: ({ cart }: { cart: CartResult }) => (
    <cart-badge>
      Cart: <span>{cart.count}</span>
    </cart-badge>
  ),
});
```

The product list is keyed. You author ordinary TSX `key` identity, and the compiler lowers it to
`kovo-key` in the emitted IR because item identity is shared by the morph layer (the runtime's DOM
patcher), template stamps, inferred fragment target suffixes, and optimistic reordering:

```tsx
export const ProductList = component({
  queries: { products: productsQuery },
  render: ({ products }: { products: ProductsResult }) => (
    <ul style={productListStyles.list}>
      {products.items.map((item) => (
        <li key={item.id}>
          {item.name} — {formatPrice(item.unitPrice)} ({item.stock} in stock)
        </li>
      ))}
    </ul>
  ),
});
```

The compiler derives the runtime wiring from these declarations: `queries:` becomes an `kovo-deps`
stamp on each island, and `{cart.count}` becomes a typed `data-bind` path. Binding paths
type-check against the query's inferred shape — rename `count` and every referencing template
goes red; bind through a nullable segment without `?.` and you get a compile error. The step's
test pins all of it from the rendered page:

```ts
it('serves compiler-derived dependency and binding stamps', async () => {
  const html = await renderShopRoute();

  // SPEC §9.1/§10.2: query dependencies are semantic { name, key? } facts; HTML attribute order
  // is not part of the wire contract. These unparameterized queries carry exact unkeyed names.
  const cartAttrs = requiredOpeningTagAttributes(html, 'cart-badge');
  expect(
    decodeFrameworkQueryDependencyToken(requiredHtmlAttribute(cartAttrs, 'kovo-deps')),
  ).toEqual({ name: cartQuery.key });
  expect(requiredHtmlAttribute(cartAttrs, 'kovo-fragment-target')).toBe('cart-badge');
  expect(requiredHtmlAttribute(cartAttrs, 'kovo-live-component')).toBe(
    'components/cart-badge/cart-badge',
  );

  const productListAttrs = requiredOpeningTagAttributes(html, 'ul');
  expect(requiredHtmlAttribute(productListAttrs, 'kovo-c')).toBe('product-list');
  expect(
    decodeFrameworkQueryDependencyToken(requiredHtmlAttribute(productListAttrs, 'kovo-deps')),
  ).toEqual({ name: productsQuery.key });

  // {cart.count} became a typed data-bind path the loader can re-run.
  const countAttrs = requiredOpeningTagAttributes(html, 'span');
  expect(requiredHtmlAttribute(countAttrs, 'data-bind')).toBe('cart.count');
  expect(html).toMatch(/<span\b[^>]*>0<\/span>/u);
});
```

## Ship data once, as shared truth

Query values are server-owned and shared: the page ships each value exactly once as a JSON
script, and every island that depends on it reads from that single copy. There's no
per-component fetch and no client cache with a lifecycle. When a value changes, the loader
replaces it and walks the self-describing bindings under each dependent island:

```tsx
export const homeRoute = route('/', {
  access: publicAccess('tutorial storefront browsing'),
  page(_input, _request: ShopRequest) {
    return (
      <html>
        <head>
          <title>Kovo Shop</title>
        </head>
        <body>
          <main>
            <h1>Kovo Shop</h1>
            <CartBadge />
            <ProductList />
          </main>
        </body>
      </html>
    );
  },
});
```

```ts
it('renders loaded query values through the declared components', async () => {
  const db = createShopDb();
  db.cartItems.push({ productId: 'p1', qty: 2, unitPrice: 1499 });
  const html = await renderShopRoute(db);

  expect(html).toContain('<span data-bind="cart.count">2</span>');
  expect(html).toContain('Pour-over kettle — $14.99 (5 in stock)');
});
```

Note what the page does **not** contain: no serialized component tree, no hydration script, no
framework boot. The data is inspectable JSON, the dependencies are attributes, and the update
plan is the DOM. The [queries guide](/guides/queries/) covers parameterized queries, instance
keys, and the typed read endpoint when you need them.

Live data now flows, and every data-to-DOM dependency is an attribute you can read. Next:
writes — and the form-shaped contract they ride in on.

<details>
<summary>Spec & diagnostics</summary>

Domains as invalidation currency: SPEC §10.1. Derived downstream surfaces and read-set
extraction: SPEC §10.2. Read set as the entire registration: Constitution #2 (no API requires
global knowledge at a local site). Derived `kovo-deps`/`data-bind` stamps and binding type-check:
SPEC §4.8; binding through a nullable segment without `?.` is **KV227**. Authored `key` lowering to
runtime `kovo-key`: SPEC §4.8, §13.2. Explicit default-deny access decisions: SPEC §10.2,
**KV436**. Data shipped once as shared truth: SPEC §4.2.

</details>
