/ Docs

Queries

What is a Query?

A query reads data from a read model. Queries provide a structured, validated way to fetch data from views, separating the data access logic from the transformation logic.

Queries in Bounda

Queries live inside read/<view-name>/queries/ and are defined as individual TypeScript files. Each query file exports three things:

  • payload — a Zod schema for validating query parameters.
  • repository — a function that accesses the read adapter directly.
  • handler — a function that transforms the repository result into the final response.
read/
  order-summary/
    view.ts
    projections/
    queries/
      get-order-summary.ts
      list-orders-by-customer.ts

Defining a Query

SQLite Example

import type { Query } from "./+types/get-order-summary";

export const payload: Query.PayloadFunction = ({ z }) =>
  z.object({ orderId: z.string().uuid() });

export const repository: Query.Repository = ({ orderId, client }) => {
  return client.prepare("SELECT * FROM order_summary WHERE id = ?").get(orderId) ?? null;
};

export async function handler({ repositoryData }: Query.HandlerArgs) {
  return repositoryData ?? null;
}

MongoDB Memory Example

import type { Query } from "./+types/get-users";

export const payload: Query.PayloadFunction = ({ z }) =>
  z.object({
    page: z.number().positive().default(1),
    pageSize: z.number().positive().default(100),
  });

export const repository: Query.Repository = async ({ page, pageSize, client }) => {
  const offset = (page - 1) * pageSize;
  return await client.collection("users_directory")
    .find({}, { projection: { _id: 0 } })
    .sort({ registeredAt: -1 })
    .skip(offset)
    .limit(pageSize)
    .toArray();
};

export function handler({ query, repositoryData }: Query.HandlerArgs) {
  return {
    data: repositoryData,
    pagination: { page: query.page, pageSize: query.pageSize },
  };
}

Query Components

Payload

The payload function defines and validates the query parameters. Bounda validates the incoming data before the repository function runs:

export const payload: Query.PayloadFunction = ({ z }) =>
  z.object({
    customerId: z.string(),
    status: z.enum(["placed", "confirmed", "shipped"]).optional(),
  });

Repository

The repository function receives the validated payload fields as individual arguments plus a client object. The client is adapter-specific — its API depends on which read adapter is configured for the view:

export const repository: Query.Repository = ({ customerId, status, client }) => {
  if (status) {
    return client.prepare("SELECT * FROM order_summary WHERE customerId = ? AND status = ?")
      .all(customerId, status);
  }
  return client.prepare("SELECT * FROM order_summary WHERE customerId = ?")
    .all(customerId);
};

Handler

The handler function receives the original query payload and the data returned by the repository. Use it to transform, enrich, or reshape the result:

export function handler({ query, repositoryData }: Query.HandlerArgs) {
  return {
    orders: repositoryData,
    filter: { customerId: query.customerId },
  };
}

Dispatching Queries

After bootstrapping the application, queries are available on the app.queries object. The method name is the camelCase form of the query file name:

const summary = await app.queries.getOrderSummary({ orderId: "order-123" });

const users = await app.queries.getUsers({ page: 1, pageSize: 50 });

Guidelines

  • Keep the repository function focused on data access. Place any transformation or business logic in the handler.
  • Use payload defaults (e.g., z.number().default(1)) to provide sensible fallbacks for optional parameters.
  • The repository function can be synchronous or asynchronous depending on the adapter.
  • Design queries around consumer needs. A single view can support multiple queries with different filtering and shaping logic.
  • Views — define the schema that queries read from
  • Projections — populate the data that queries return