Skip to content

Query handlers ​

A query handler is the application service of one read. It returns a view read through a query repository, without loading the aggregate. It saves nothing and publishes nothing.

ts
export class GetOrderSummaryHandler extends QueryHandler<GetOrderSummary, OrderSummary, OrderNotFound> {
	async handle({ orderId }: GetOrderSummary): Promise<Result<OrderSummary, OrderNotFound>> {
		const summary = await this.summaries.summaryOf(new OrderId(orderId));
		return summary === undefined ? err(new OrderNotFound({ orderId })) : ok(summary);
	}
}

When to use ​

Write one query handler per read a client needs: an order summary, a list of orders. A view has the shape the reader wants, whatever the shape of the aggregate. Requests that change state go through a command handler.

Usage ​

Declare the query and its handler ​

The query is a plain type named after the request, in the same file as its handler.

src/ordering/application/queries/get-order-summary.query.ts
ts
import { err, ok, QueryHandler, type Result } from "@alveolus/core";

import { OrderNotFound } from "../../domain/errors/order-not-found.error";
import { OrderSummaries } from "../../domain/repositories/order-summaries.repository";
import { OrderId } from "../../domain/value-objects/order-id.identifier";
import type { OrderSummary } from "../../domain/views/order-summary.view";

export interface GetOrderSummary {
	readonly orderId: string;
}

export class GetOrderSummaryHandler extends QueryHandler<GetOrderSummary, OrderSummary, OrderNotFound> {
	constructor(private readonly summaries: OrderSummaries) {
		super();
	}

	async handle({ orderId }: GetOrderSummary): Promise<Result<OrderSummary, OrderNotFound>> {
		const summary = await this.summaries.summaryOf(new OrderId(orderId));
		return summary === undefined ? err(new OrderNotFound({ orderId })) : ok(summary);
	}
}

Return a list ​

A query that cannot fail keeps the default Error of never.

src/ordering/application/queries/list-orders.query.ts
ts
export class ListOrdersHandler extends QueryHandler<void, readonly OrderSummary[]> {
	constructor(private readonly summaries: OrderSummaries) {
		super();
	}

	async handle(): Promise<Result<readonly OrderSummary[], never>> {
		return ok(await this.summaries.all());
	}
}

Keep to the query side ​

A query handler reads views through query repositories. It never receives a command repository, an outbox, a unit of work or an event publisher: a read must not change anything.

❌ Avoid
ts
constructor(
	private readonly orders: Orders,
	private readonly unitOfWork: UnitOfWork,
) {
	super();
}
✅ Prefer
ts
constructor(private readonly summaries: OrderSummaries) {
	super();
}

Checked by command-query-separation.

Reference ​

ts
abstract class QueryHandler<Input, Output, Error extends AnyDomainError = never> {
	abstract handle(query: Input): Promise<Result<Output, Error>>;
}
Type parameterDescription
InputThe query: the data the handler needs.
OutputWhat the read returns, usually a view or a list of views.
ErrorUnion of the domain errors the handler may return. Defaults to never.
MemberTypeDescription
handle(query)Promise<Result<Output, Error>>Runs the read and returns its outcome.

Caveats

  • There is no separate read model: views are read from the same storage, through a query repository.
  • Call super() in the constructor of your handler.

Import from @alveolus/core or @alveolus/core/query-handlers.

See also ​

Released under the MIT License.