CQRS (Command Query Responsibility Segregation) implementation for AdonisJS
npm install @wailroth/cqrs

> CQRS (Command Query Responsibility Segregation) implementation for AdonisJS
A clean, type-safe CQRS implementation for AdonisJS with support for pipeline behaviors, automatic handler registration, and Result types for error handling.
- Command Bus - For write operations that modify state
- Query Bus - For read operations that query data
- Pipeline Behaviors - Cross-cutting concerns like logging, validation, caching, transactions
- Result Types - Functional error handling with ok(), err(), isOk(), isErr()
- Automatic Registration - Handlers auto-register with the bus using base classes
- Type-Safe - Full TypeScript support with proper type inference
- AdonisJS Integration - Seamless integration with AdonisJS IoC container
``bash`
npm install @wailroth/cqrs
Then configure the package:
`bash`
node ace configure @wailroth/cqrs
This will:
- Register the CQRS provider in your adonisrc.ts`
- Create the recommended directory structure:
`
app/
└── application/
├── commands/
├── queries/
└── handlers/
`ts
// app/application/commands/create_user.ts
import type { ICommand } from '@wailroth/cqrs'
export interface CreateUser extends ICommand {
email: string
name: string
password: string
}
`
`ts
// app/application/handlers/create_user_command_handler.ts
import { inject } from '@adonisjs/core'
import { CommandHandlerBase, ok } from '@wailroth/cqrs'
import type { CreateUser } from '../commands/create_user.js'
import type { Result } from '@wailroth/cqrs'
@inject()
export class CreateUserCommandHandler extends CommandHandlerBase
async handle(command: CreateUser): Promise
// Create the user in your database
// await User.create(command)
return ok()
}
}
`
`ts
import { inject } from '@adonisjs/core'
import { CommandBus } from '@wailroth/cqrs'
@inject()
export class UserService {
constructor(private commandBus: CommandBus) {}
async createUser(data: { email: string; name: string; password: string }) {
const result = await this.commandBus.execute({
email: data.email,
name: data.name,
password: data.password,
})
if (result.isOk()) {
console.log('User created successfully')
} else {
console.error('Failed to create user:', result.error)
}
}
}
`
Queries work similarly but return data directly:
`ts
// app/application/queries/get_user.ts
export interface GetUser extends IQuery {
userId: number
}
// app/application/handlers/get_user_query_handler.ts
@inject()
export class GetUserQueryHandler extends QueryHandlerBase
async handle(query: GetUser): Promise
return await User.find(query.userId)
}
}
// Usage
const user = await queryBus.execute({ userId: 1 })
`
Commands return a Result type for error handling:
`ts
import { ok, err, isOk, isErr } from '@wailroth/cqrs'
// Success
ok() // Result
ok(data) // Result
// Error
err(['Error message'])
errMessage('Error message')
// Checking
if (isOk(result)) {
result.data // T
}
if (isErr(result)) {
result.error // string[]
}
`
Add cross-cutting concerns using behaviors:
`ts`
import {
LoggingCommandBehavior,
TransactionCommandBehavior,
ValidationCommandBehavior,
CacheQueryBehavior,
LoggingQueryBehavior,
} from '@wailroth/cqrs'
`ts
// In a service provider or boot method
import { CommandBus } from '@wailroth/cqrs'
import { LoggingCommandBehavior } from '@wailroth/cqrs/services'
commandBus.use(new LoggingCommandBehavior(logger))
`
`ts
import type { CommandBehavior } from '@wailroth/cqrs'
export class AuditBehavior implements CommandBehavior {
async handle
command: TCommand,
next: (cmd: TCommand) => Promise
): Promise
const startTime = Date.now()
const result = await next(command)
const duration = Date.now() - startTime
await AuditLog.create({
command: command.constructor.name,
duration,
success: result.isOk(),
})
return result
}
}
`
If you don't want to use the base classes:
`ts
import { CommandBus } from '@wailroth/cqrs'
import type { ICommandHandler } from '@wailroth/cqrs'
@inject()
export class MyHandler implements ICommandHandler
async handle(command: MyCommand): Promise
// ...
}
}
// In a provider
commandBus.register('MyCommand', new MyHandler())
`
`ts
import { ValidationCommandBehavior } from '@wailroth/cqrs'
import vine from '@vinejs/vine'
const schema = vine.object({
email: vine.string().email(),
name: vine.string().minLength(3),
})
commandBus.use(
new ValidationCommandBehavior(async (command) => {
return vine.validate({ schema, data: command })
})
)
`
Requires @adonisjs/lucid:
`ts
import { TransactionCommandBehavior } from '@wailroth/cqrs'
import Database from '@adonisjs/lucid/database'
commandBus.use(new TransactionCommandBehavior(Database))
`
The recommended structure (created automatically by configure):
``
app/
└── application/
├── commands/ # ICommand definitions
├── queries/ # IQuery definitions
└── handlers/ # Handler implementations
- Commands: {Action}{Entity}Command (e.g., CreateUserCommand){Action}{Entity}Query
- Queries: (e.g., GetUserQuery){CommandOrQueryName}Handler
- Handlers: (e.g., CreateUserCommandHandler`)
The base classes automatically extract the command/query name from the handler class name for registration.
MIT
For issues and questions, please use the GitHub issue tracker.