typebus-cqrs
v0.2.0
Published
Simple, type-safe CQRS library with auto-registration and minimal boilerplate (typebus-cqrs)
Maintainers
Readme
TypeBus-CQRS
[TYPEBUS-CQRS] ||||| o o
Simple, type-safe CQRS library with auto-registration and minimal boilerplate
🎯 Why TypeBus-CQRS?
Problem with existing CQRS libraries:
- 🤯 Too much boilerplate code
- 📦 Multiple imports for simple operations
- 🔨 Separate command and handler creation
- 🔧 Manual registration required
TypeBus-CQRS solution:
- ✅ One import for everything
- ✅ Command + handler in a single line
- ✅ Auto-registration on creation
- ✅ Full type safety with TypeScript
- ✅ Minimal boilerplate - 3x less code
- ✅ Extensible types - define your own commands/queries/events
🚀 Ready for Use:
- ✅ TypeScript 5.0+ support
- ✅ Middleware pipeline
- ✅ Built-in logging
- ✅ Full test coverage
- ✅ Production ready
- ✅ Framework agnostic
- ✅ Extensible type system
🚀 Quick Start
Installation
npm install typebus-cqrs reflect-metadata
# or
yarn add typebus-cqrs reflect-metadataBasic Usage
import { createTypeBus, createCommand, createQuery } from 'typebus-cqrs';
// 1. Create TypeBus-CQRS instance
const bus = createTypeBus();
// 2. Create command with auto-registration (one line!)
const CreateUser = createCommand(
bus,
'User.CreateUser',
async (data, aggregateId) => {
// Your business logic here
console.log(`Creating user: ${data.name}`);
return { userId: aggregateId, events: ['User.Created'] };
}
);
// 3. Create query with auto-registration
const GetUser = createQuery(
bus,
'User.GetUser',
async (params) => {
// Your query logic here
return { id: params.userId, name: 'John Doe', email: '[email protected]' };
}
);
// 4. Use them!
async function example() {
// Execute command
const result = await CreateUser.execute(
{ name: 'Alice', email: '[email protected]', password: 'secret' },
'user-123'
);
// Execute query
const user = await GetUser.execute({ userId: 'user-123' });
console.log('User created:', result);
console.log('User retrieved:', user);
}That's it! No separate classes, no manual registration, no complex setup.
🔧 Extensible Type System
TypeBus-CQRS now supports extensible types, allowing you to define your own command, query, and event types without being limited by built-in types.
Define Your Custom Types
import { createTypeBus, createCommand, createQuery, ExtendCommandMap, ExtendQueryMap } from 'typebus-cqrs';
// Define your custom command types
interface CustomCommandMap {
'Product.CreateProduct': {
data: {
name: string;
price: number;
category: string;
};
aggregateId: string;
result: { productId: string; sku: string; events: string[] };
};
'Product.UpdatePrice': {
data: {
newPrice: number;
reason: string;
};
aggregateId: string;
result: { success: boolean; oldPrice: number; newPrice: number };
};
}
// Define your custom query types
interface CustomQueryMap {
'Product.GetProduct': {
params: { productId: string };
result: {
id: string;
name: string;
price: number;
category: string;
};
};
'Product.SearchProducts': {
params: {
searchTerm?: string;
category?: string;
minPrice?: number;
maxPrice?: number;
};
result: {
products: Array<{ id: string; name: string; price: number }>;
total: number;
};
};
}
// Extend the built-in types with your custom types
type ExtendedCommandMap = ExtendCommandMap<CustomCommandMap>;
type ExtendedQueryMap = ExtendQueryMap<CustomQueryMap>;
// Create TypeBus with your extended types
const bus = createTypeBus<ExtendedCommandMap, ExtendedQueryMap>();
// Now you can use your custom types with full type safety!
const CreateProduct = createCommand(
bus,
'Product.CreateProduct', // ← TypeScript knows this type!
async (data, aggregateId) => {
// data is typed as { name: string; price: number; category: string; }
const sku = `SKU-${Date.now()}`;
return { productId: aggregateId, sku, events: ['Product.Created'] };
}
);
const GetProduct = createQuery(
bus,
'Product.GetProduct', // ← TypeScript knows this type!
async (params) => {
// params is typed as { productId: string }
return {
id: params.productId,
name: 'Sample Product',
price: 29.99,
category: 'Electronics'
};
}
);
// Usage with full type safety
await CreateProduct.execute(
{ name: 'Laptop', price: 999.99, category: 'Electronics' }, // ← Validated!
'prod-123'
);
const product = await GetProduct.execute({ productId: 'prod-123' }); // ← Validated!Alternative: Dynamic Types (Less Type Safety, More Flexibility)
If you prefer more flexibility over strict typing:
const bus = createTypeBus(); // Uses 'any' types
const CreateOrder = createCommand(
bus,
'Order.CreateOrder' as any, // Use string literals
async (data: any, aggregateId: string) => {
return { orderId: aggregateId, status: 'pending' };
}
);
// No compile-time type checking, but maximum flexibility
await CreateOrder.execute({ userId: 'user-123', items: [] }, 'order-456');📚 Documentation
Core Concepts
Commands
Commands represent write operations that change state:
const UpdateProfile = createCommand(
bus,
'User.UpdateProfile',
async (data, aggregateId, metadata) => {
// Validation
if (!data.name && !data.email) {
throw new Error('At least one field must be updated');
}
// Business logic
await updateUserInDatabase(aggregateId, data);
// Publish event
await UserProfileUpdated.publish(
{ changes: data },
aggregateId,
2 // version
);
return { success: true };
}
);
// Usage
await UpdateProfile.execute(
{ name: 'New Name', email: '[email protected]' },
'user-123',
{ updatedBy: 'admin' } // metadata
);Queries
Queries represent read operations:
const SearchUsers = createQuery(
bus,
'User.SearchUsers',
async (params) => {
const { searchTerm, page = 1, limit = 10 } = params;
// Query logic
const users = await searchInDatabase(searchTerm, page, limit);
return {
users,
total: users.length,
page,
hasMore: users.length === limit
};
}
);
// Usage
const results = await SearchUsers.execute({
searchTerm: 'john',
page: 1,
limit: 20
});Events
Events represent things that have happened:
const UserCreated = createEventHandler(
bus,
'User.Created',
async (data, aggregateId, version, metadata) => {
// Update read model
await updateUserReadModel(aggregateId, data);
// Send welcome email
await sendWelcomeEmail(data.email, data.name);
// Track analytics
await analytics.track('user_created', {
userId: aggregateId,
source: metadata?.source
});
}
);
// Publishing events
await UserCreated.publish(
{ name: 'John', email: '[email protected]' },
'user-123',
1
);Alternative Usage Patterns
1. Direct Bus Usage
// Type-safe direct execution
const result = await bus.executeCommand(
'User.CreateUser', // ← Auto-completion!
{ name: 'John', email: '[email protected]', password: 'secret' },
'user-123'
);
const user = await bus.executeQuery(
'User.GetUser', // ← Type-checked!
{ userId: 'user-123' }
);2. Fluent API
import { fluent } from 'typebus-cqrs';
const userCommands = fluent(bus)
.command('User.CreateUser')
.handle(async (data, aggregateId) => {
// Handler logic
return { userId: aggregateId, events: [] };
});
const userQueries = fluent(bus)
.query('User.GetUser')
.handle(async (params) => {
// Query logic
return { id: params.userId, name: 'John' };
});3. Batch Registration
const userHandlers = fluent(bus)
.batch()
.addCommand('createUser', 'User.CreateUser', async (data, id) => {
return { userId: id, events: [] };
})
.addQuery('getUser', 'User.GetUser', async (params) => {
return { id: params.userId, name: 'John' };
})
.build();
// Usage
await userHandlers.createUser.execute(data, 'user-123');
const user = await userHandlers.getUser.execute({ userId: 'user-123' });Middleware
TypeBus-CQRS supports middleware for cross-cutting concerns:
import { withLogging } from 'typebus-cqrs';
const bus = createTypeBus();
// Add logging middleware
bus.use(withLogging({
level: 'info',
includeData: true
}));
// Custom middleware
bus.use({
async execute(message, next) {
console.log(`Executing: ${message.type}`);
try {
const result = await next(message);
console.log(`Success: ${message.type}`);
return result;
} catch (error) {
console.error(`Error in ${message.type}:`, error.message);
throw error;
}
}
});Type Safety
TypeBus-CQRS provides full type safety through TypeScript:
// 1. Define your message types
interface CommandMap {
'User.CreateUser': {
data: { name: string; email: string; password: string };
aggregateId: string;
result: { userId: string; events: string[] };
};
}
// 2. TypeScript automatically infers and validates everything
const CreateUser = createCommand(
bus,
'User.CreateUser', // ← Must match CommandMap key
async (data, aggregateId) => {
// data is automatically typed as { name: string; email: string; password: string }
// aggregateId is automatically typed as string
// Return type must match CommandMap['User.CreateUser']['result']
return { userId: aggregateId, events: ['User.Created'] };
}
);
// 3. Usage is also type-safe
await CreateUser.execute(
{ name: 'John', email: '[email protected]', password: 'secret' }, // ← Validated!
'user-123'
);🧪 Testing
TypeBus-CQRS is designed to be easily testable:
describe('User Commands', () => {
let bus: TypeBus;
beforeEach(() => {
bus = new TypeBus({ enableLogging: false }); // Clean instance per test
});
afterEach(() => {
bus.clear(); // Clean up
});
it('should create user', async () => {
const CreateUser = createCommand(
bus,
'User.CreateUser',
async (data, aggregateId) => {
return { userId: aggregateId, events: ['User.Created'] };
}
);
const result = await CreateUser.execute(
{ name: 'Test User', email: '[email protected]', password: 'secret' },
'test-user-1'
);
expect(result.userId).toBe('test-user-1');
expect(result.events).toContain('User.Created');
});
});🔌 Framework Integration
Express.js
import express from 'express';
import { createTypeBus, createCommand } from 'typebus-cqrs';
const app = express();
const bus = createTypeBus();
// Register commands
const CreateUser = createCommand(bus, 'User.CreateUser', async (data, id) => {
return { userId: id, events: [] };
});
// Use in routes
app.post('/users', async (req, res) => {
try {
const result = await CreateUser.execute(req.body, `user-${Date.now()}`);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});NestJS
import { Injectable } from '@nestjs/common';
import { createTypeBus } from 'typebus-cqrs';
@Injectable()
export class UserService {
private bus = createTypeBus();
constructor() {
this.setupCommands();
}
private setupCommands() {
this.createUser = createCommand(
this.bus,
'User.CreateUser',
async (data, aggregateId) => {
// Business logic
return { userId: aggregateId, events: [] };
}
);
}
}📊 Comparison with Other Libraries
| Feature | TypeBus-CQRS | NestJS CQRS | Wolkenkit | EventStore | |---------|-------------|-------------|-----------|------------| | Simplicity | ✅ 10/10 | ❌ 6/10 | ❌ 4/10 | ❌ 5/10 | | Minimal Code | ✅ 10/10 | ❌ 4/10 | ❌ 3/10 | ❌ 4/10 | | Type Safety | ✅ 9/10 | ✅ 9/10 | ⚠️ 7/10 | ⚠️ 7/10 | | Auto Registration | ✅ 10/10 | ❌ 5/10 | ❌ 4/10 | ❌ 4/10 | | Learning Curve | ✅ Easy | ⚠️ Medium | ❌ Hard | ⚠️ Medium | | Extensible Types | ✅ 10/10 | ❌ 3/10 | ❌ 2/10 | ❌ 3/10 |
Before (NestJS CQRS):
// 1. Create command class
export class CreateUserCommand {
constructor(public readonly name: string, public readonly email: string) {}
}
// 2. Create handler class
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
async execute(command: CreateUserCommand): Promise<void> {
// Logic here
}
}
// 3. Register in module
@Module({
providers: [CreateUserHandler],
})
export class UserModule {}
// 4. Use with imports
import { CreateUserCommand } from './commands/CreateUserCommand';
const command = new CreateUserCommand('John', '[email protected]');
await this.commandBus.execute(command);After (TypeBus-CQRS):
// 1. One line creates command + handler + registration
const CreateUser = createCommand(bus, 'User.CreateUser', async (data, id) => {
// Logic here
});
// 2. Use directly - no imports needed
await CreateUser.execute({ name: 'John', email: '[email protected]' }, 'user-123');TypeBus-CQRS - Because CQRS should be simple, not complicated. 🚌✨
🛠️ Advanced Features
Custom Middleware
// Validation middleware
const validationMiddleware = {
async execute(message, next) {
// Validate message based on type
if (message.type === 'User.CreateUser') {
const { data } = message as any;
if (!data.email?.includes('@')) {
throw new Error('Invalid email');
}
}
return await next(message);
}
};
bus.use(validationMiddleware);Metrics and Monitoring
const bus = createTypeBus({
enableLogging: true,
enableMetrics: true
});
// Get stats
console.log(bus.getStats());
// {
// commandHandlers: 5,
// queryHandlers: 3,
// eventHandlers: 8,
// middleware: 2
// }Error Handling
const CreateUser = createCommand(
bus,
'User.CreateUser',
async (data, aggregateId) => {
try {
// Business logic
const user = await userService.create(data);
// Publish success event
await UserCreated.publish(user, aggregateId, 1);
return { userId: user.id, events: ['User.Created'] };
} catch (error) {
// Publish failure event
await UserCreationFailed.publish(
{ reason: error.message, data },
aggregateId,
1
);
throw error;
}
}
);📝 License
MIT License
Copyright (c) 2024 Bayramov TG
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
🔮 Roadmap
- [ ] MongoDB adapter for event store
- [ ] Redis adapter for caching
- [ ] Saga pattern support
- [ ] GraphQL integration
- [ ] OpenTelemetry integration
- [ ] VS Code extension for auto-completion
⚡ Setup & Usage
1. Install Dependencies
npm install
# or
yarn install2. Run Examples
Basic Example
npm run example:basic
# or
npm run devAdvanced Example
npm run example:advancedExtensible Types Example
npm run example:extensible3. Development
# Build the project
npm run build
# Build with watch mode
npm run build:watch
# Run tests
npm test
# Watch tests
npm run test:watch
# Check test coverage
npm run test:coverage
# Linting
npm run lint
# Auto-fix lint issues
npm run lint:fix
# Code formatting
npm run format🧪 Quick Test
After installing dependencies, run:
npm run example:basicYou should see something like:
🚀 TypeBus-CQRS Basic Usage Example
📦 Creating commands, queries, and event handlers...
▶️ Executing commands and queries...
1. Creating user...
📤 SUCCESS: User.CreateUser { id: 'cmd-...', duration: '102.34ms' }
✅ Result: { userId: 'user-001', events: ['User.Created'] }
2. Getting user...
📥 SUCCESS: User.GetUser { id: 'qry-...', duration: '51.23ms' }
✅ User: { id: 'user-001', name: 'John Doe', email: '[email protected]' }
✅ Basic example completed!