Defining Contracts
Defining Contracts
Section titled “Defining Contracts”Service contracts define the interface between your service and the outside world. The platform automatically generates REST, GraphQL, and message bus endpoints from contract definitions.
Use This Guide If…
Section titled “Use This Guide If…”- You’re creating a new microservice and need to define its API
- You want to understand how contracts become APIs
- You need to add permissions to your endpoints
- You’re defining commands, queries, or events for your service
- You want compile-time type safety for your service APIs
Quick Example
Section titled “Quick Example”import { Command } from '@banyanai/platform-contract-system';
@Command({ description: 'Create a new user account', permissions: ['users:create']})export class CreateUserCommand { email!: string; firstName!: string; lastName!: string;}
export interface CreateUserResult { userId: string; email: string; createdAt: string;}Automatically generates:
- REST:
POST /api/create-user - GraphQL:
mutation { createUser(input: ...) { ... } } - Message bus: Contract routing with permission validation
- JSON Schema: For input validation
Contract Types
Section titled “Contract Types”Commands
Section titled “Commands”Commands represent write operations - create, update, delete, process.
import { Command } from '@banyanai/platform-contract-system';
@Command({ description: 'Create a new order', permissions: ['orders:create']})export class CreateOrderCommand { customerId!: string; items!: OrderItem[]; totalAmount!: number;}
export interface CreateOrderResult { orderId: string; status: string; createdAt: string;}
interface OrderItem { productId: string; quantity: number; price: number;}Queries
Section titled “Queries”Queries represent read operations - get, find, list, search.
import { Query } from '@banyanai/platform-contract-system';
@Query({ description: 'Retrieve user by ID', permissions: ['users:read']})export class GetUserQuery { userId!: string;}
export interface UserResult { userId: string; email: string; firstName: string; lastName: string; createdAt: string;}Events
Section titled “Events”Events represent things that happened - UserCreated, OrderShipped, PaymentProcessed.
import { DomainEvent } from '@banyanai/platform-contract-system';
@DomainEvent('User.Events.UserCreated', { broadcast: true, description: 'User account was created'})export class UserCreatedEvent { userId!: string; email!: string; createdAt!: string;}Decorator Options
Section titled “Decorator Options”Command Decorator
Section titled “Command Decorator”@Command({ description: string; // Human-readable description (required) permissions: string[]; // Required permissions (required, can be empty)})Example:
@Command({ description: 'Update product information', permissions: ['products:update']})export class UpdateProductCommand { productId!: string; name?: string; price?: number;}Query Decorator
Section titled “Query Decorator”@Query({ description: string; // Human-readable description (required) permissions: string[]; // Required permissions (required, can be empty)})Example:
@Query({ description: 'List orders with pagination', permissions: ['orders:read']})export class ListOrdersQuery { page?: number; pageSize?: number; status?: string;}Event Decorator
Section titled “Event Decorator”@DomainEvent(name: string, options?: { broadcast?: boolean; // Broadcast to WebSocket/GraphQL subscriptions description?: string; // Human-readable description})Example:
@DomainEvent('Notification.Events.NotificationSent', { broadcast: true, description: 'Notification was sent to user'})export class NotificationSentEvent { notificationId!: string; userId!: string; channel!: 'email' | 'sms' | 'push'; sentAt!: string;}Input and Output Schemas
Section titled “Input and Output Schemas”Input Schema (Command/Query)
Section titled “Input Schema (Command/Query)”The class definition IS the input schema:
@Command({ description: 'Create user', permissions: ['users:create']})export class CreateUserCommand { // All properties define the input schema email!: string; // Required string firstName!: string; // Required string lastName!: string; // Required string age?: number; // Optional number role?: 'admin' | 'user'; // Optional enum}Generated JSON schema:
{ "type": "object", "properties": { "email": { "type": "string" }, "firstName": { "type": "string" }, "lastName": { "type": "string" }, "age": { "type": "number" }, "role": { "type": "string", "enum": ["admin", "user"] } }, "required": ["email", "firstName", "lastName"]}Output Schema (Result Interface)
Section titled “Output Schema (Result Interface)”Export an interface for the result:
export interface CreateUserResult { userId: string; email: string; createdAt: string;}Handler returns this interface:
@CommandHandlerDecorator(CreateUserCommand)export class CreateUserHandler extends CommandHandler<CreateUserCommand, CreateUserResult> { async handle(command: CreateUserCommand, user: AuthenticatedUser | null): Promise<CreateUserResult> { return { userId: 'user-123', email: command.email, createdAt: new Date().toISOString() }; }}Permission Validation
Section titled “Permission Validation”Required Permissions
Section titled “Required Permissions”Declared in contract decorator:
@Command({ description: 'Permanently delete order', permissions: ['orders:delete', 'admin:all'] // User needs EITHER permission})export class DeleteOrderCommand { orderId!: string;}Platform validates at API Gateway (Layer 1):
- Extracts user permissions from JWT token
- Checks if user has ANY of the required permissions
- Rejects request if no matching permissions
- Creates message if validation passes
Multiple Permissions (OR Logic)
Section titled “Multiple Permissions (OR Logic)”@Command({ description: 'Generate financial report', permissions: [ 'reports:generate', 'finance:admin', 'admin:all' ]})export class GenerateReportCommand { reportType!: string;}User needs ANY of:
reports:generatefinance:adminadmin:all
No Permissions (Public)
Section titled “No Permissions (Public)”@Query({ description: 'Health check endpoint', permissions: [] // No authentication required})export class GetHealthQuery {}Contract Broadcasting
Section titled “Contract Broadcasting”Automatic Broadcasting
Section titled “Automatic Broadcasting”When your service starts:
await BaseService.start({ name: 'user-service', version: '1.0.0'});Platform automatically:
- Discovers all contracts in packages/contracts
- Generates JSON schemas from TypeScript types
- Broadcasts contracts to service discovery
- Registers with API Gateway
- Updates routing tables
Log output:
Contract broadcast complete: 3 contracts- User.Commands.CreateUser- User.Queries.GetUser- User.Events.UserCreatedType Safety
Section titled “Type Safety”Compile-Time Validation
Section titled “Compile-Time Validation”Handler and contract types must match:
@Command({ description: 'Create user', permissions: ['users:create']})export class CreateUserCommand { email!: string; name!: string;}
// ✅ Correct - types match@CommandHandlerDecorator(CreateUserCommand)export class CreateUserHandler extends CommandHandler<CreateUserCommand, CreateUserResult> { async handle(command: CreateUserCommand, user: AuthenticatedUser | null): Promise<CreateUserResult> { // TypeScript knows command.email and command.name exist return { userId: '123', email: command.email }; }}
// ❌ Compile error - wrong command type@CommandHandlerDecorator(UpdateUserCommand)export class CreateUserHandler extends CommandHandler<CreateUserCommand, CreateUserResult> { // Error: Decorator argument doesn't match handler type}Runtime Validation
Section titled “Runtime Validation”Platform validates incoming requests:
// Invalid request (missing required field)POST /api/create-user{ "email": "test@example.com" // Missing: firstName, lastName}
// Platform responds{ "error": "ValidationError", "message": "Missing required fields: firstName, lastName", "fields": ["firstName", "lastName"]}Best Practices
Section titled “Best Practices”Naming Conventions
Section titled “Naming Conventions”DO:
// Use descriptive namesexport class CreateUserCommand {}export class GetUserQuery {}export class UserCreatedEvent {}
// Use PascalCaseexport class UpdateProductCommand {}export class SearchOrdersQuery {}DON’T:
// Vague namesexport class UserCommand {}export class DataQuery {}
// Wrong casingexport class create_user_command {}export class get-user-query {}Contract Organization
Section titled “Contract Organization”DO:
packages/├── contracts/│ ├── src/│ │ ├── commands.ts # All commands│ │ ├── queries.ts # All queries│ │ ├── events.ts # All events│ │ └── index.ts # Re-export all│ └── package.jsonDON’T:
packages/├── contracts/│ ├── src/│ │ ├── CreateUserCommand.ts # One file per contract│ │ ├── GetUserQuery.ts│ │ ├── UserCreatedEvent.tsRequired vs Optional Fields
Section titled “Required vs Optional Fields”DO:
export class CreateUserCommand { email!: string; // Required (!) firstName!: string; // Required (!) lastName!: string; // Required (!) age?: number; // Optional (?)}DON’T:
export class CreateUserCommand { email: string; // ❌ Not explicitly required or optional firstName: string | undefined; // ❌ Verbose}Result Interfaces
Section titled “Result Interfaces”DO:
// Simple interface for resultsexport interface CreateUserResult { userId: string; email: string; createdAt: string;}DON’T:
// Complex classes for resultsexport class CreateUserResult { constructor( public userId: string, public email: string, public createdAt: string ) {}
toJSON() { /* ... */ }}Advanced Patterns
Section titled “Advanced Patterns”Nested Objects
Section titled “Nested Objects”@Command({ description: 'Create order with items', permissions: ['orders:create']})export class CreateOrderCommand { customerId!: string; items!: OrderItem[]; // Array of objects shippingAddress!: Address; // Nested object}
interface OrderItem { productId: string; quantity: number; price: number;}
interface Address { street: string; city: string; state: string; zipCode: string;}Enums and Union Types
Section titled “Enums and Union Types”@Command({ description: 'Update user role', permissions: ['users:update-role']})export class UpdateUserRoleCommand { userId!: string; role!: 'admin' | 'manager' | 'user'; // Union type status!: UserStatus; // Enum}
enum UserStatus { ACTIVE = 'active', INACTIVE = 'inactive', SUSPENDED = 'suspended'}Optional Fields for Updates
Section titled “Optional Fields for Updates”@Command({ description: 'Update user information', permissions: ['users:update']})export class UpdateUserCommand { userId!: string; // Required firstName?: string; // Optional - only update if provided lastName?: string; // Optional email?: string; // Optional}Complete Example
Section titled “Complete Example”import { Command } from '@banyanai/platform-contract-system';
@Command({ description: 'Create a new product', permissions: ['products:create']})export class CreateProductCommand { name!: string; description!: string; price!: number; categoryId!: string;}
export interface CreateProductResult { productId: string; name: string; createdAt: string;}
@Command({ description: 'Update product information', permissions: ['products:update']})export class UpdateProductCommand { productId!: string; name?: string; description?: string; price?: number;}
export interface UpdateProductResult { productId: string; updatedFields: string[]; updatedAt: string;}import { Query } from '@banyanai/platform-contract-system';
@Query({ description: 'Retrieve product by ID', permissions: ['products:read']})export class GetProductQuery { productId!: string;}
export interface ProductResult { productId: string; name: string; description: string; price: number; categoryId: string; createdAt: string; updatedAt: string;}
@Query({ description: 'List products with pagination', permissions: ['products:read']})export class ListProductsQuery { page?: number; pageSize?: number; categoryId?: string;}
export interface ListProductsResult { products: ProductResult[]; page: number; pageSize: number; totalCount: number; totalPages: number;}import { DomainEvent } from '@banyanai/platform-contract-system';
@DomainEvent('Product.Events.ProductCreated', { broadcast: true, description: 'Product was created'})export class ProductCreatedEvent { productId!: string; name!: string; price!: number; createdAt!: string;}
@DomainEvent('Product.Events.ProductUpdated', { broadcast: true, description: 'Product was updated'})export class ProductUpdatedEvent { productId!: string; updatedFields!: string[]; updatedAt!: string;}export * from './commands.js';export * from './queries.js';export * from './events.js';Troubleshooting
Section titled “Troubleshooting””Contract not found”
Section titled “”Contract not found””- Check contract is exported from
packages/contracts/src/index.ts - Check decorator is present (
@Command,@Query, or@DomainEvent) - Check service broadcast contracts on startup
- Verify service discovery is running
”Handler not discovered”
Section titled “”Handler not discovered””- Check handler decorator references correct contract class
- Check handler is in correct directory
- Check handler filename ends with
Handler.ts
”Type mismatch”
Section titled “”Type mismatch””- Check handler type parameter matches contract class
- Check result interface matches handler return type
Related Resources
Section titled “Related Resources”- Writing Handlers - Implementing contract handlers
- Using Service Clients - Calling other service contracts
- Testing Services - Testing contracts and handlers
Next Steps:
- Create your contract package structure
- Define commands, queries, and events
- Implement handlers for each contract
- Test your contracts thoroughly