CQRS Composables & Handlers: Bridging UI and Business Logic
CQRS Composables & Handlers: Bridging UI and Business Logic
Section titled “CQRS Composables & Handlers: Bridging UI and Business Logic”In standard Vue.js applications, components are frequently overloaded with disparate responsibilities: managing UI state (loaders, error messages), executing HTTP requests, handling business logic, and orchestrating component lifecycle events. This widespread anti-pattern results in monolithic, untestable .vue files that are extremely difficult to maintain or refactor.
Xeno Vue enforces a strict Separation of Concerns by dividing these responsibilities across two distinct architectural primitives: Vue Composables (Presentation Layer) and CQRS Handlers (Application Layer).
The Architectural Boundary
Section titled “The Architectural Boundary”To protect the core business domain from UI framework churn, Xeno establishes a firm boundary between reactivity and execution:
- The Composable (
use-*.ts): Acts as the reactive ViewModel. It manages Vue-specific state (reffor loading, error, and data), handles component lifecycle hooks (onUnmounted), and orchestrates safe execution via theClientMediator. - The Handler (
*.handler.ts): Acts as the pure Application logic. It is a completely framework-agnostic TypeScript class that receives an isolated payload (Command or Query) and anAbortSignal, executes the required operations (like invoking a Remote Data Source), and returns a deterministicResultType<T>monad.
By splitting these layers, you ensure that your business logic can be unit-tested in pure Node.js environments without ever mounting a DOM or mocking Vue reactivity systems.
The Anatomy of a Xeno CQRS Composable
Section titled “The Anatomy of a Xeno CQRS Composable”Xeno’s CLI generates highly optimized Vue Composables designed to safely bridge the gap between user interactions and the IoC container. A standard Xeno Composable manages four critical areas:
1. Reactive State Management
Section titled “1. Reactive State Management”The composable encapsulates the localized state required by the UI, such as loading flags, error strings, and the resulting data payload. This prevents global state pollution (e.g., using Pinia for ephemeral request states) and keeps the component template clean.
2. Type-Safe IoC Resolution
Section titled “2. Type-Safe IoC Resolution”Instead of importing API clients directly, the composable uses ServicesUtils.useApp() to resolve both the ClientMediator and the specific CQRS Handler required for the operation directly from the frontend Dependency Injection container.
3. Cooperative Cancellation (AbortController)
Section titled “3. Cooperative Cancellation (AbortController)”Network requests in SPAs often lead to memory leaks or state corruption if a user navigates away from a page before a slow API call completes.
To prevent this, the composable automatically instantiates an AbortController before executing the mediator pipeline. It exposes an abort() function to the UI for manual cancellation and automatically binds it to Vue’s onUnmounted lifecycle hook, guaranteeing that pending requests are severed when the component is destroyed.
4. Monadic Error Unwrapping
Section titled “4. Monadic Error Unwrapping”The composable evaluates the Result monad returned by the mediator. If the operation fails, it extracts the error message and exposes it to the UI reactively; if it succeeds, it updates the data ref safely.
Practical Implementation Example
Section titled “Practical Implementation Example”Below is an example of a Command and its matching Composable, exactly as generated by the Xeno CLI engine.
1. The Pure TypeScript Handler
Section titled “1. The Pure TypeScript Handler”The handler is an isolated class resolved by the DI container. It accepts the AbortSignal and performs the actual remote mutation. Notice the complete absence of Vue imports (ref, reactive, etc.).
import type { ResultType } from '@xeno-js/vue';import { Result } from '@xeno-js/vue';import type { CreateUserCommand } from './create-user.command';import type { CreateUserResponse } from './create-user.model';
export class CreateUserHandler { constructor( // Injected Remote Data Sources from the IoC Container ) {}
public async handle(command: CreateUserCommand, signal?: AbortSignal): Promise<ResultType<CreateUserResponse>> { // Implement your frontend business logic or API calls here. // Example: return await this.dataSource.post('/api/users', command.payload, { signal }); return Result.ok(); }}2. The Vue Composable
Section titled “2. The Vue Composable”The composable acts as the reactive proxy. It connects the Vue component to the ClientMediator and handles the AbortController lifecycle.
import { ref, onUnmounted } from 'vue';import { Result } from '@xeno-js/vue';import { ServicesUtils } from '@/use-app';import { CreateUserCommand } from './create-user.command';import type { CreateUserRequest, CreateUserResponse } from './create-user.model';
export function useCreateUser() { const loading = ref(false); const error = ref<string | null>(null); let abortController: AbortController | null = null;
const execute = async (payload: CreateUserRequest): Promise<Result<CreateUserResponse>> => { // Prevent overlapping executions if (loading.value) return Result.fail(new Error('Already executing'));
abortController = new AbortController(); loading.value = true; error.value = null;
try { // Resolve the Mediator and Handler from the Xeno IoC Container const { mediator, CREATE_USER_HANDLER: handler } = ServicesUtils.useApp();
const command = new CreateUserCommand(payload);
// Dispatch through the Mediator, passing the cancellation signal const result = await mediator.send(command, async () => { return await handler.handle(command, abortController!.signal); });
// Unwrap the result and bind errors to the reactive UI state if (!result.isOk()) { error.value = result.getErrorOrThrow().message; }
return result; } finally { loading.value = false; abortController = null; } };
// Expose manual cancellation const abort = () => { if (abortController) { abortController.abort(); } };
// Automatically cancel pending operations when the component unmounts onUnmounted(() => { abort(); });
return { loading, error, execute, abort };}Consuming the Composable in the UI
Section titled “Consuming the Composable in the UI”With the separation established, your Vue component (.vue) becomes incredibly thin and declarative. It only needs to invoke the composable and bind the reactive variables to the template:
<script setup lang="ts">import { useCreateUser } from '@/features/users/use-create-user.composable';
const { loading, error, execute, abort } = useCreateUser();
const submitForm = async () => { const result = await execute({ name: 'John Doe', email: 'john@example.com' });
if (result.isOk()) { // Navigate or show success toast }};</script>
<template> <form @submit.prevent="submitForm"> <!-- UI bindings are clean and direct --> <div v-if="error" class="error-banner">{{ error }}</div>
<button type="submit" :disabled="loading"> {{ loading ? 'Saving...' : 'Create User' }} </button>
<button type="button" @click="abort" v-if="loading"> Cancel </button> </form></template>By adhering to this pattern, you eliminate technical debt from the presentation layer, enforce strict memory management through cooperative cancellation, and ensure that your core business capabilities remain perfectly isolated.
Support Us
Section titled “Support Us”Xeno is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section