Skip to main content

NestJS Integration

This guide demonstrates how to integrate state machines with NestJS for robust backend workflow management and business logic.

Basic Service Integration

Create a state machine service with dependency injection:

types/order.types.ts
export interface OrderContext {
orderId: string;
customerId: string;
amount: number;
items: Array<{ id: string; quantity: number; price: number }>;
paymentMethod: string;
shippingAddress: any;
createdAt: Date;
updatedAt: Date;
metadata: Record<string, any>;
}

export type OrderState =
| 'PENDING'
| 'PAYMENT_PROCESSING'
| 'PAID'
| 'PREPARING'
| 'SHIPPED'
| 'DELIVERED'
| 'CANCELLED'
| 'REFUNDED';

export type OrderEvent =
| 'process_payment'
| 'payment_success'
| 'payment_failed'
| 'prepare'
| 'ship'
| 'deliver'
| 'cancel'
| 'refund';

Database Integration with TypeORM

Integrate state machines with database entities:

entities/order.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { OrderState, OrderContext } from './order-state-machine.service';

@Entity('orders')
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
customerId: string;

@Column('decimal', { precision: 10, scale: 2 })
amount: number;

@Column('json')
items: Array<{ id: string; quantity: number; price: number }>;

@Column()
paymentMethod: string;

@Column('json', { nullable: true })
shippingAddress: any;

@Column({
type: 'enum',
enum: [
'PENDING',
'PAYMENT_PROCESSING',
'PAID',
'PREPARING',
'SHIPPED',
'DELIVERED',
'CANCELLED',
'REFUNDED',
],
default: 'PENDING',
})
state: OrderState;

@Column('json', { default: {} })
metadata: Record<string, any>;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;

// Convert entity to state machine context
toContext(): OrderContext {
return {
orderId: this.id,
customerId: this.customerId,
amount: this.amount,
items: this.items,
paymentMethod: this.paymentMethod,
shippingAddress: this.shippingAddress,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
metadata: this.metadata || {},
};
}

// Update entity from context
fromContext(context: OrderContext): void {
this.updatedAt = context.updatedAt;
this.metadata = context.metadata;
}
}

Controller Implementation

Create REST endpoints for order management:

dto/order.dto.ts
export class CreateOrderDto {
customerId: string;
amount: number;
items: Array<{ id: string; quantity: number; price: number }>;
paymentMethod: string;
shippingAddress?: any;
}

export class ProcessEventDto {
event: OrderEvent;
}

Middleware Integration

Create middleware for logging and validation:

middleware/audit.middleware.ts
import { Injectable, Logger } from '@nestjs/common';
import { BaseMiddleware } from '@jewel998/state-machine';

@Injectable()
export class AuditMiddleware extends BaseMiddleware {
private readonly logger = new Logger(AuditMiddleware.name);

constructor() {
super('audit', { priority: -100 });
}

async onAction(context, next) {
const startTime = Date.now();

this.logger.log(`Action starting for order ${context.currentContext.orderId}`);

try {
const result = await next();
const duration = Date.now() - startTime;

this.logger.log(
`Action completed for order ${context.currentContext.orderId} in ${duration}ms`
);

return result;
} catch (error) {
const duration = Date.now() - startTime;

this.logger.error(
`Action failed for order ${context.currentContext.orderId} after ${duration}ms`,
error
);

throw error;
}
}

async onGuard(context, next) {
this.logger.log(`Guard check for order ${context.currentContext.orderId}`);

const result = await next();

this.logger.log(`Guard result for order ${context.currentContext.orderId}: ${result}`);

return result;
}
}

@Injectable()
export class ValidationMiddleware extends BaseMiddleware {
private readonly logger = new Logger(ValidationMiddleware.name);

constructor() {
super('validation', { priority: -50 });
}

async onAction(context, next) {
// Validate context before action
const orderContext = context.currentContext;

if (!orderContext.orderId) {
throw new Error('Order ID is required');
}

if (orderContext.amount < 0) {
throw new Error('Order amount cannot be negative');
}

return await next();
}
}

Background Job Integration

Integrate with job queues for async processing:

services/order-processor.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
import { OrderService } from './order.service';
import { OrderEvent } from './order-state-machine.service';

@Injectable()
export class OrderProcessorService {
private readonly logger = new Logger(OrderProcessorService.name);

constructor(
@InjectQueue('order-processing') private orderQueue: Queue,
private readonly orderService: OrderService
) {}

async scheduleOrderEvent(orderId: string, event: OrderEvent, delay = 0) {
await this.orderQueue.add('process-order-event', { orderId, event }, { delay });

this.logger.log(`Scheduled event ${event} for order ${orderId} with delay ${delay}ms`);
}

async schedulePaymentTimeout(orderId: string, timeoutMs = 300000) {
// 5 minutes
await this.orderQueue.add('payment-timeout', { orderId }, { delay: timeoutMs });

this.logger.log(`Scheduled payment timeout for order ${orderId} in ${timeoutMs}ms`);
}

async scheduleShippingNotification(orderId: string, delayMs = 86400000) {
// 24 hours
await this.orderQueue.add('shipping-notification', { orderId }, { delay: delayMs });

this.logger.log(`Scheduled shipping notification for order ${orderId} in ${delayMs}ms`);
}
}

Module Configuration

Wire everything together in a NestJS module:

modules/order.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BullModule } from '@nestjs/bull';
import { Order } from './order.entity';
import { OrderController } from './order.controller';
import { OrderService } from './order.service';
import { OrderStateMachineService } from './order-state-machine.service';
import { OrderProcessorService } from './order-processor.service';
import { OrderProcessor } from './order-processor.processor';
import { AuditMiddleware, ValidationMiddleware } from './state-machine.middleware';

@Module({
imports: [
TypeOrmModule.forFeature([Order]),
BullModule.registerQueue({
name: 'order-processing',
}),
],
controllers: [OrderController],
providers: [
OrderService,
OrderStateMachineService,
OrderProcessorService,
OrderProcessor,
AuditMiddleware,
ValidationMiddleware,
],
exports: [OrderService, OrderStateMachineService],
})
export class OrderModule {}

Testing

Create comprehensive tests for your state machine integration:

__tests__/order.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { OrderService } from './order.service';
import { OrderStateMachineService } from './order-state-machine.service';
import { Order } from './order.entity';

describe('OrderService', () => {
let service: OrderService;
let repository: Repository<Order>;
let stateMachineService: OrderStateMachineService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
OrderService,
OrderStateMachineService,
{
provide: getRepositoryToken(Order),
useClass: Repository,
},
],
}).compile();

service = module.get<OrderService>(OrderService);
repository = module.get<Repository<Order>>(getRepositoryToken(Order));
stateMachineService = module.get<OrderStateMachineService>(OrderStateMachineService);
});

describe('processOrderEvent', () => {
it('should process payment successfully', async () => {
const order = new Order();
order.id = 'test-order-id';
order.state = 'PENDING';
order.amount = 100;
order.paymentMethod = 'credit_card';

jest.spyOn(repository, 'findOne').mockResolvedValue(order);
jest.spyOn(repository, 'save').mockResolvedValue(order);

const result = await service.processOrderEvent('test-order-id', 'process_payment');

expect(result.state).toBe('PAYMENT_PROCESSING');
expect(repository.save).toHaveBeenCalled();
});

it('should throw error for invalid transition', async () => {
const order = new Order();
order.id = 'test-order-id';
order.state = 'DELIVERED';

jest.spyOn(repository, 'findOne').mockResolvedValue(order);

await expect(service.processOrderEvent('test-order-id', 'process_payment')).rejects.toThrow(
'Cannot process event'
);
});
});
});

This NestJS integration provides a robust foundation for building complex backend workflows with state machines, including proper error handling, database persistence, background job processing, and comprehensive testing.