@hexaijs/application
v0.5.2
Published
CQRS Application Framework for Hexagonal Architecture
Downloads
617
Readme
@hexaijs/application
Application layer infrastructure for building CQRS applications with hexagonal architecture
Overview
@hexaijs/application provides the application layer components for building CQRS (Command Query Responsibility Segregation) applications. It bridges the gap between your domain model and external interfaces (HTTP, CLI, message queues) by providing a structured way to handle commands, queries, and events.
The package centers around three key patterns:
- Application - A facade that routes messages to their handlers and manages execution context
- ApplicationContext - Scopes handler execution with transactional boundaries and event publishing
- Interceptors - Cross-cutting concerns that wrap message handling (logging, tracing, authorization)
These components work together to ensure each command/query executes in isolation with proper transaction management, while events flow through the system with causation and correlation tracking.
Installation
npm install @hexaijs/applicationCore Concepts
Command and Query
Command and Query extend the core Message class to represent different intents. Commands change state; queries read state.
Both classes accept two type parameters:
Payload- The data the message carriesResultType- The output type (enables automatic type inference)
import { Command, Query } from "@hexaijs/application";
// Command - changes state, returns { orderId: string }
export class CreateOrderCommand extends Command<
{ customerId: string; items: { productId: string; quantity: number }[] },
{ orderId: string }
> {
static readonly type = "order.create-order";
}
// Query - reads state, returns OrderDto
export class GetOrderQuery extends Query<
{ orderId: string },
OrderDto
> {
static readonly type = "order.get-order";
}With output types declared, executeCommand and executeQuery automatically infer return types:
const result = await app.executeCommand(new CreateOrderCommand({
customerId: "c-123",
items: []
}));
// result type: Result<{ orderId: string }> - automatically inferred!
const orderResult = await app.executeQuery(new GetOrderQuery({ orderId: "o-456" }));
// orderResult type: Result<OrderDto> - automatically inferred!CommandHandler and QueryHandler
Handlers implement the execute method to process messages. The output type is automatically inferred from the Command/Query's ResultType parameter:
import { CommandHandler, QueryHandler } from "@hexaijs/application";
interface OrderContext {
getOrderRepository(): OrderRepository;
}
// Handler infers output type from CreateOrderCommand's ResultType ({ orderId: string })
class CreateOrderHandler implements CommandHandler<CreateOrderCommand, OrderContext> {
async execute(
command: CreateOrderCommand,
ctx: OrderContext
): Promise<{ orderId: string }> {
const repository = ctx.getOrderRepository();
const order = Order.create(command.getPayload());
await repository.add(order);
return { orderId: order.getId().getValue() };
}
}
// Handler infers output type from GetOrderQuery's ResultType (OrderDto)
class GetOrderHandler implements QueryHandler<GetOrderQuery, OrderContext> {
async execute(
query: GetOrderQuery,
ctx: OrderContext
): Promise<OrderDto> {
const repository = ctx.getOrderRepository();
const order = await repository.get(query.getPayload().orderId);
return toDto(order);
}
}You can also extract output types directly from Command/Query classes using indexed access:
type CreateOrderOutput = CreateOrderCommand['ResultType']; // { orderId: string }
type GetOrderOutput = GetOrderQuery['ResultType']; // OrderDtoEventHandler
Event handlers react to domain events. The canHandle method filters which events the handler processes:
import { Message } from "@hexaijs/core";
import { EventHandler } from "@hexaijs/application";
class SendOrderConfirmationEmail implements EventHandler<OrderPlaced, OrderContext> {
canHandle(message: Message): boolean {
return message.getMessageType() === OrderPlaced.getType();
}
async handle(event: OrderPlaced, ctx: OrderContext): Promise<void> {
const payload = event.getPayload();
await ctx.getEmailService().send({
to: payload.customerEmail,
template: "order-confirmation",
data: { orderId: payload.orderId }
});
}
}ApplicationBuilder
ApplicationBuilder assembles handlers and context into an Application using a fluent API:
import { ApplicationBuilder } from "@hexaijs/application";
const application = new ApplicationBuilder()
.withApplicationContext(new OrderApplicationContext())
.withCommandHandler(CreateOrderCommand, () => new CreateOrderHandler())
.withCommandHandler(CancelOrderCommand, () => new CancelOrderHandler())
.withQueryHandler(GetOrderQuery, () => new GetOrderHandler())
.withEventHandler(() => new SendOrderConfirmationEmail())
.build();Each handler is registered with a factory function. This allows fresh handler instances per request, preventing state leakage between requests.
Application
The Application interface provides three methods for dispatching messages:
import { Application, Result } from "@hexaijs/application";
// Execute commands
const result: Result<{ orderId: string }> = await application.executeCommand(
new CreateOrderCommand({ customerId: "c-123", items: [] })
);
// Execute queries
const orderResult: Result<OrderDto> = await application.executeQuery(
new GetOrderQuery({ orderId: "order-456" })
);
// Handle events (typically called by infrastructure)
await application.handleEvent(orderPlacedEvent);Result
All operations return a Result type that is either SuccessResult or ErrorResult:
const result = await application.executeCommand(command);
if (result.isSuccess) {
console.log("Order created:", result.data.orderId);
}
if (result.isError) {
console.error("Failed:", result.error.message);
}
// Or throw on error
const data = result.getOrThrow();ApplicationContext
ApplicationContext is a marker interface that your application context must implement. Define your context as a plain interface with the dependencies your handlers need:
import { ApplicationContext } from "@hexaijs/application";
import { UnitOfWork } from "@hexaijs/core";
export interface OrderApplicationContext extends ApplicationContext {
getUnitOfWork(): UnitOfWork;
getOrderRepository(): OrderRepository;
getEmailService(): EmailService;
}Event publishing is handled by ApplicationEventPublisher, which the Application wires automatically. In handlers, publish events through the application's event infrastructure:
// Inside a handler — events receive causation/correlation headers automatically
await ctx.publish(
new OrderPlaced({
orderId: order.getId().getValue(),
customerId: command.getPayload().customerId
})
);ExecutionScope
ExecutionScope provides ALS-based (AsyncLocalStorage) execution context that scopes handler execution with SecurityContext, correlation, and causation data. It replaces the old pattern of passing SecurityContext through message generics.
import { ExecutionScope } from "@hexaijs/application";
// Wrap handler execution with security context
await ExecutionScope.run(
{ securityContext: { userId: "user-123", roles: ["admin"] } },
async () => {
// Inside this scope, security context is available
const user = ExecutionScope.requireSecurityContext<UserSecurityContext>();
console.log(user.userId); // "user-123"
}
);Accessing Scope Data
// Optional access (returns undefined if not in scope)
const sc = ExecutionScope.getSecurityContext<UserSecurityContext>();
// Required access (throws if not in scope)
const sc = ExecutionScope.requireSecurityContext<UserSecurityContext>();
// Correlation and causation
const correlation = ExecutionScope.getCorrelation();
const causation = ExecutionScope.getCausation();Snapshot and Restore
For async boundaries (e.g., spawning background tasks), capture and restore scope:
// Capture current scope
const snapshot = ExecutionScope.snapshot();
// Later, in a different async context
if (snapshot) {
await ExecutionScope.restore(snapshot, async () => {
// Security context and trace data restored
const sc = ExecutionScope.requireSecurityContext();
});
}Parent Scope Inheritance
Nested run() calls inherit from the parent scope. Only explicitly provided fields override parent values:
await ExecutionScope.run({ securityContext: user }, async () => {
await ExecutionScope.run({ correlation: trace }, async () => {
// securityContext: inherited from parent (user)
// correlation: overridden (trace)
});
});Interceptors
Interceptors wrap message handling for cross-cutting concerns. They follow a middleware pattern:
import { CommandInterceptor, CommandInterceptionContext, Result } from "@hexaijs/application";
const loggingInterceptor: CommandInterceptor = async (
ctx: CommandInterceptionContext,
next: () => Promise<Result<unknown>>
): Promise<Result<unknown>> => {
console.log("Executing:", ctx.message.getMessageType());
const start = Date.now();
const result = await next();
console.log(`Completed in ${Date.now() - start}ms`);
return result;
};
// Register with builder
const app = new ApplicationBuilder()
.withApplicationContext(context)
.withCommandHandler(CreateOrderCommand, () => new CreateOrderHandler())
.withCommandInterceptor(loggingInterceptor)
.build();Four interceptor types are available:
| Type | Applies To |
|------|------------|
| CommandInterceptor | Command execution only |
| QueryInterceptor | Query execution only |
| EventInterceptor | Event handling only |
| Interceptor | All message types |
Advanced Patterns
SelectorBasedEventHandler
For handlers that respond to multiple event types, use SelectorBasedEventHandler with the @When decorator:
import { Message } from "@hexaijs/core";
import { SelectorBasedEventHandler, When, eventTypeMatches } from "@hexaijs/application";
class OrderEventHandler extends SelectorBasedEventHandler<Message, OrderContext> {
@When(eventTypeMatches(OrderPlaced.getType()))
async onOrderPlaced(event: OrderPlaced, ctx: OrderContext): Promise<void> {
// Handle order placed
}
@When(eventTypeMatches(OrderCancelled.getType()))
async onOrderCancelled(event: OrderCancelled, ctx: OrderContext): Promise<void> {
// Handle order cancelled
}
}The eventTypeMatches helper supports strings, arrays, and regex patterns:
@When(eventTypeMatches("order.order-placed"))
@When(eventTypeMatches(["order.order-placed", "order.order-updated"]))
@When(eventTypeMatches(/^order\./))SimpleCompositeApplication
Compose multiple bounded context applications into one:
import { SimpleCompositeApplication } from "@hexaijs/application";
const compositeApp = new SimpleCompositeApplication({
"order.": orderApplication, // handles order.* messages
"inventory.": inventoryApplication,
"shipping.": shippingApplication,
});
// Routes to appropriate application based on message type prefix
await compositeApp.executeCommand(new CreateOrderCommand(...));For event handling across bounded contexts, provide a UnitOfWork to ensure all handlers execute in the same transaction. handleEvent uses scope() internally for lazy transaction management:
compositeApp.setUnitOfWork(unitOfWork);
await compositeApp.handleEvent(event);
// All event handlers run within a single scope() transactionAPI Highlights
| Export | Description |
|--------|-------------|
| Application | Interface for command/query execution and event handling |
| ApplicationBuilder | Fluent builder for assembling applications |
| Command<P, O> | Base class for commands with payload and output type |
| Query<P, O> | Base class for queries with payload and output type |
| ExecutionScope | ALS-based execution context for security context, correlation, and causation |
| ExecutionScopeSnapshot | Immutable snapshot of execution scope data |
| CommandHandler<I, Ctx> | Interface for command handlers (output inferred from command) |
| QueryHandler<I, Ctx> | Interface for query handlers (output inferred from query) |
| EventHandler<E, Ctx> | Interface for event handlers |
| ApplicationContext | Marker interface for application contexts |
| Result<R, E> | Union type of SuccessResult or ErrorResult |
| SelectorBasedEventHandler | Base class for decorator-based event routing |
| SimpleCompositeApplication | Composes multiple applications by message prefix |
| CommandInterceptor | Interceptor type for commands |
| QueryInterceptor | Interceptor type for queries |
| EventInterceptor | Interceptor type for events |
| Interceptor | Interceptor type for all messages |
Migration Guide
From v0.4.0 to v0.5.0
This version replaces message-level SecurityContext with ALS-based ExecutionScope and simplifies the application context.
Command and Query
Before (v0.4.x):
class MyCommand extends Command<Payload, ResultType, MySecurityContext> {}
const sc = command.getSecurityContext(); // from messageAfter (v0.5.0):
class MyCommand extends Command<Payload, ResultType> {}
const sc = ExecutionScope.requireSecurityContext<MySecurityContext>(); // from ALSSecurityContext Access in Handlers
Before (v0.4.x):
async execute(command: MyCommand, ctx: MyContext) {
const user = command.getSecurityContext();
}After (v0.5.0):
import { ExecutionScope } from "@hexaijs/application";
async execute(command: MyCommand, ctx: MyContext) {
const user = ExecutionScope.requireSecurityContext<UserSecurityContext>();
}ApplicationContext
AbstractApplicationContext replaced with ApplicationContext marker interface. Define contexts as plain interfaces instead of extending an abstract class.
Quick Migration Checklist
- [ ] Replace
Command<P, O, SC>withCommand<P, O> - [ ] Replace
Query<P, O, SC>withQuery<P, O> - [ ] Replace
command.getSecurityContext()/command.withSecurityContext()withExecutionScopemethods - [ ] Remove
MessageWithAuthandMessageWithAuthOptionsimports - [ ] Remove
clone(),deriveFrom(),onEnter(),onExit(),enterCommandExecutionScope()overrides from ApplicationContext subclasses - [ ] Add
@hexaijs/contracts^0.1.0as peer dependency
From v0.3.1 to v0.4.0
This version promotes correlation/causation tracing to Message-level API in @hexaijs/core.
Correlation/Causation Headers
Before (v0.3.x) - using raw headers:
const cmd = new MyCommand(payload)
.withHeader("correlationId", "corr-123")
.withHeader("correlationType", "HttpRequest");After (v0.4.0) - using typed methods:
const cmd = new MyCommand(payload)
.withCorrelation({ id: "corr-123", type: "HttpRequest" });
cmd.getCorrelation(); // { id: "corr-123", type: "HttpRequest" }The utility functions (causationOf, correlationOf, setCausationOf, setCorrelationOf, asTrace) and MessageTrace type from messaging-support have been removed. Use Message methods and import MessageTrace from @hexaijs/core.
Quick Migration Checklist
- [ ] Replace
import { MessageTrace } from "@hexaijs/application"withimport { MessageTrace } from "@hexaijs/core" - [ ] Replace
import { asTrace, causationOf, ... } from "@hexaijs/application"withMessagemethods - [ ] Replace
withHeader("correlationId", ...)/withHeader("correlationType", ...)withwithCorrelation({ id, type }) - [ ] Replace
withHeader("causationId", ...)/withHeader("causationType", ...)withwithCausation({ id, type }) - [ ] Replace
getHeader("correlationId")withgetCorrelation()?.id - [ ] Replace
causationOf(msg)withmsg.getCausation() - [ ] Replace
correlationOf(msg)withmsg.getCorrelation() - [ ] Replace
asTrace(msg)withmsg.asTrace() - [ ] Update
@hexaijs/corepeer dependency to^0.6.0
From v0.2.0 to v0.3.0
Note:
MessageWithAuthand constructor options described here were removed in v0.5.0. See v0.4.0 → v0.5.0 migration guide.
This version changes the constructor pattern for Command, Query, and MessageWithAuth from positional parameters to an options object.
Constructor Pattern
Before (v0.2.x):
const command = new CreateOrderCommand(payload, headers, securityContext);After (v0.3.0):
const command = new CreateOrderCommand(payload, {
headers,
securityContext,
});Clone Methods
clone(), cloneWithHeaders(), and withHeader() overrides have been removed from MessageWithAuth. These methods are now inherited from the Message base class in @hexaijs/core. No migration needed — the public API (withHeader(), withSecurityContext()) remains the same.
Quick Migration Checklist
- [ ] Update
new Command(payload, headers, sc)tonew Command(payload, { headers, securityContext: sc }) - [ ] Update
new Query(payload, headers, sc)tonew Query(payload, { headers, securityContext: sc }) - [ ] Remove any direct calls to
clone()orcloneWithHeaders()onMessageWithAuth(usewithHeader()/withSecurityContext()instead)
From v0.1.x to v0.2.0
Note: The
SecurityContext(SC) generic described here was removed in v0.5.0. Command is nowCommand<P, O>. See v0.4.0 → v0.5.0 migration guide.
This version introduces automatic output type inference with breaking changes to type parameters.
Command and Query
Before (v0.1.x):
// Second type parameter was SecurityContext
class MyCommand extends Command<Payload, MySecurityContext> {}After (v0.2.0):
// Second type parameter is now ResultType, SecurityContext moves to third
class MyCommand extends Command<Payload, ResultType, MySecurityContext> {}
// If you don't need custom SecurityContext, just use two parameters:
class MyCommand extends Command<Payload, ResultType> {}CommandHandler and QueryHandler
Before (v0.1.x):
class MyHandler implements CommandHandler<MyCommand, OutputType, Context> {
async execute(cmd: MyCommand, ctx: Context): Promise<OutputType> { ... }
}After (v0.2.0):
// Output type is automatically inferred from MyCommand's ResultType
class MyHandler implements CommandHandler<MyCommand, Context> {
async execute(cmd: MyCommand, ctx: Context): Promise<OutputType> { ... }
}Security Context Access
Before (v0.1.x):
// SC was the second type parameter
class MyCommand extends Command<Payload, MySecurityContext> {}
const sc = command.getSecurityContext(); // MySecurityContextAfter (v0.2.0):
// SC is now the third type parameter
class MyCommand extends Command<Payload, ResultType, MySecurityContext> {}
const sc = command.getSecurityContext(); // MySecurityContext (same usage)Quick Migration Checklist
- [ ] Update
Command<P, SC>toCommand<P, O, SC>orCommand<P, O>(SC moves to 3rd position) - [ ] Update
Query<P, SC>toQuery<P, O, SC>orQuery<P, O>(SC moves to 3rd position) - [ ] Remove
Oparameter fromCommandHandler<I, O, Ctx>→CommandHandler<I, Ctx> - [ ] Remove
Oparameter fromQueryHandler<I, O, Ctx>→QueryHandler<I, Ctx>
See Also
- @hexaijs/core - Core domain primitives this package builds upon
- @hexaijs/postgres - PostgreSQL implementation for ApplicationContext
- @hexaijs/plugin-application-builder - Decorators for automatic handler registration
