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
repositoryfunction focused on data access. Place any transformation or business logic in thehandler. - Use
payloaddefaults (e.g.,z.number().default(1)) to provide sensible fallbacks for optional parameters. - The
repositoryfunction 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.
Related Concepts
- Views — define the schema that queries read from
- Projections — populate the data that queries return