# NestJS Implementation Plan for AutoStore ## Overview Implementation of AutoStore system using NestJS with TypeScript, following enterprise-grade DDD (Domain-Driven Design), Clean Architecture, and Hexagonal Architecture principles. This system will manage items with expiration dates and automatically reorder when items expire, demonstrating best practices for scalable, maintainable enterprise applications. ## Architecture Approach - **Domain-Driven Design (DDD)** with rich domain models, aggregates, bounded contexts, and domain events - **Clean Architecture** with strict dependency inversion and clear layer boundaries - **Hexagonal Architecture** with ports and adapters for external integrations - **CQRS (Command Query Responsibility Segregation)** for separating read and write operations - **Event-Driven Architecture** with domain events for cross-cutting concerns - **Dependency Injection** leveraging NestJS IoC container with proper module organization - **Test-Driven Development** approach with comprehensive unit and integration tests ## Business Domain Analysis ### Core Domain Concepts 1. **Item Aggregate**: Represents a stored item with business rules around expiration and reordering 2. **User Aggregate**: Represents system users with authentication and authorization 3. **ItemExpiration Domain Service**: Handles expiration logic and reordering business rules 4. **Domain Events**: ItemExpired, ItemCreated, ItemDeleted for event-driven communication ### Business Rules (Domain) 1. Each item has a name, expiration date, and order URL 2. Expired items are automatically removed and reordered 3. Expired items can be added, triggering immediate ordering 4. Every item belongs to a user with ownership validation 5. Only the item's owner can manage it 6. System must check all items for expiration on startup ## Layered Architecture Implementation ### 1. Domain Layer (Core Business Logic) #### 1.1 Aggregates **File**: `src/domain/aggregates/item.aggregate.ts` **Purpose**: Root aggregate for Item entity with business logic and invariants ```typescript export class ItemAggregate { private readonly _id: ItemId; private readonly _name: string; private readonly _expirationDate: ExpirationDate; private readonly _orderUrl: OrderUrl; private readonly _userId: UserId; private readonly _createdAt: DateTime; private _domainEvents: DomainEvent[] = []; constructor( id: ItemId, name: string, expirationDate: ExpirationDate, orderUrl: OrderUrl, userId: UserId ) { this._id = id; this._name = name; this._expirationDate = expirationDate; this._orderUrl = orderUrl; this._userId = userId; this._createdAt = DateTime.now(); this.validate(); } // Business methods with domain events public checkExpiration(currentTime: DateTime): boolean { if (this._expirationDate.isExpired(currentTime)) { this.addDomainEvent(new ItemExpiredEvent( this._id, this._name, this._userId, this._orderUrl )); return true; } return false; } // Domain event management private addDomainEvent(event: DomainEvent): void { this._domainEvents.push(event); } public pullDomainEvents(): DomainEvent[] { const events = this._domainEvents; this._domainEvents = []; return events; } // Getters with value objects get id(): ItemId { return this._id; } get name(): string { return this._name; } get expirationDate(): ExpirationDate { return this._expirationDate; } get orderUrl(): OrderUrl { return this._orderUrl; } get userId(): UserId { return this._userId; } get createdAt(): DateTime { return this._createdAt; } private validate(): void { if (!this._name || this._name.trim().length === 0) { throw new InvalidItemDataException('Item name cannot be empty'); } // Additional business invariants } } ``` **File**: `src/domain/aggregates/user.aggregate.ts` **Purpose**: User aggregate with authentication and authorization logic #### 1.2 Value Objects **File**: `src/domain/value-objects/item-id.vo.ts` **Purpose**: Strongly typed item identifier with validation ```typescript export class ItemId { private readonly _value: string; constructor(value: string) { if (!this.isValidUuid(value)) { throw new InvalidItemIdException('Invalid Item ID format'); } this._value = value; } private isValidUuid(value: string): boolean { // UUID v4 validation const uuidRegex = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; return uuidRegex.test(value); } get value(): string { return this._value; } equals(other: ItemId): boolean { return this._value === other._value; } toString(): string { return this._value; } } ``` **File**: `src/domain/value-objects/expiration-date.vo.ts` **Purpose**: Immutable expiration date with business logic ```typescript export class ExpirationDate { private readonly _value: DateTime; constructor(value: Date | DateTime) { const dateTime = DateTime.isDateTime(value) ? value : DateTime.fromJSDate(value); if (dateTime <= DateTime.now()) { throw new InvalidExpirationDateException('Expiration date must be in the future'); } this._value = dateTime; } isExpired(currentTime: DateTime): boolean { return this._value <= currentTime; } daysUntilExpiration(currentTime: DateTime): number { return Math.ceil(this._value.diff(currentTime, 'days').days); } get value(): DateTime { return this._value; } toISOString(): string { return this._value.toISO(); } } ``` #### 1.3 Domain Services **File**: `src/domain/services/item-expiration.domain-service.ts` **Purpose**: Domain service for complex expiration logic ```typescript export class ItemExpirationDomainService { constructor( private readonly timeProvider: ITimeProvider ) {} public processItemExpiration(item: ItemAggregate): ItemExpirationResult { const currentTime = this.timeProvider.now(); const isExpired = item.checkExpiration(currentTime); if (isExpired) { return new ItemExpirationResult( true, new OrderRequest( item.id, item.name, item.userId, item.orderUrl, currentTime ) ); } return new ItemExpirationResult(false, null); } public getExpiredItemsSpecification(currentTime: DateTime): Specification { return new ItemExpirationSpecification(currentTime); } } ``` #### 1.4 Domain Events **File**: `src/domain/events/item-expired.event.ts` **Purpose**: Domain event for item expiration ```typescript export class ItemExpiredEvent implements DomainEvent { readonly occurredOn: DateTime; readonly eventId: string; readonly aggregateId: string; readonly version: number = 1; constructor( public readonly itemId: ItemId, public readonly itemName: string, public readonly userId: UserId, public readonly orderUrl: OrderUrl ) { this.occurredOn = DateTime.now(); this.eventId = uuidv4(); this.aggregateId = itemId.value; } getEventName(): string { return 'item.expired'; } } ``` #### 1.5 Specifications **File**: `src/domain/specifications/item-expiration.spec.ts` **Purpose**: Specification pattern for complex queries ```typescript export class ItemExpirationSpecification implements Specification { constructor(private readonly currentTime: DateTime) {} isSatisfiedBy(candidate: ItemAggregate): boolean { return candidate.expirationDate.isExpired(this.currentTime); } // For TypeORM/Query Builder integration toCriteria(): object { return { expirationDate: { $lte: this.currentTime.toISO() } }; } } ``` ### 2. Application Layer (Use Cases) #### 2.1 Commands (Write Operations) **File**: `src/application/commands/add-item/add-item.command.ts` **Purpose**: Command for creating new items ```typescript export class AddItemCommand { constructor( public readonly name: string, public readonly expirationDate: string, public readonly orderUrl: string, public readonly userId: string ) {} } ``` **File**: `src/application/commands/add-item/add-item.command-handler.ts` **Purpose**: Command handler with CQRS pattern ```typescript @CommandHandler(AddItemCommand) export class AddItemCommandHandler implements ICommandHandler { constructor( private readonly itemRepository: IItemRepository, private readonly expirationDomainService: ItemExpirationDomainService, private readonly eventBus: IEventBus, private readonly logger: Logger ) {} async execute(command: AddItemCommand): Promise { try { const itemAggregate = new ItemAggregate( new ItemId(uuidv4()), command.name, new ExpirationDate(new Date(command.expirationDate)), new OrderUrl(command.orderUrl), new UserId(command.userId) ); // Check for immediate expiration const expirationResult = this.expirationDomainService.processItemExpiration(itemAggregate); if (expirationResult.isExpired) { // Publish domain event for expired item await this.eventBus.publish(itemAggregate.pullDomainEvents()); return null; // Item expired immediately, not stored } // Save non-expired item await this.itemRepository.save(itemAggregate); // Publish ItemCreated event await this.eventBus.publish(itemAggregate.pullDomainEvents()); this.logger.log(`Item created: ${itemAggregate.id.value}`); return itemAggregate.id.value; } catch (error) { this.logger.error(`Failed to add item: ${error.message}`); throw new ApplicationError('Failed to add item', error); } } } ``` #### 2.2 Queries (Read Operations) **File**: `src/application/queries/get-item/get-item.query.ts` **Purpose**: Query for retrieving item details ```typescript export class GetItemQuery { constructor( public readonly itemId: string, public readonly userId: string ) {} } ``` **File**: `src/application/queries/get-item/get-item.query-handler.ts` **Purpose**: Query handler with CQRS pattern ```typescript @QueryHandler(GetItemQuery) export class GetItemQueryHandler implements IQueryHandler { constructor( private readonly itemRepository: IItemRepository, private readonly logger: Logger ) {} async execute(query: GetItemQuery): Promise { try { const item = await this.itemRepository.findById(new ItemId(query.itemId)); if (!item) { throw new ItemNotFoundException(`Item not found: ${query.itemId}`); } // Ownership validation if (!item.userId.equals(new UserId(query.userId))) { throw new UnauthorizedAccessException('User does not own this item'); } return this.mapToItemDto(item); } catch (error) { this.logger.error(`Failed to get item: ${error.message}`); throw new ApplicationError('Failed to get item', error); } } private mapToItemDto(item: ItemAggregate): ItemDto { return new ItemDto( item.id.value, item.name, item.expirationDate.toISOString(), item.orderUrl.value, item.userId.value, item.createdAt.toISO() ); } } ``` #### 2.3 DTOs **File**: `src/application/dto/item.dto.ts` **Purpose**: Data transfer objects for API communication ```typescript export class ItemDto { constructor( public readonly id: string, public readonly name: string, public readonly expirationDate: string, public readonly orderUrl: string, public readonly userId: string, public readonly createdAt: string ) {} } export class CreateItemRequestDto { @IsString() @IsNotEmpty() @MaxLength(255) name: string; @IsString() @IsNotEmpty() @IsISO8601() expirationDate: string; @IsString() @IsNotEmpty() @IsUrl() orderUrl: string; } ``` ### 3. Infrastructure Layer (Technical Implementation) #### 3.1 Persistence **File**: `src/infrastructure/persistence/typeorm/entities/item.typeorm-entity.ts` **Purpose**: TypeORM entity for database persistence ```typescript @Entity('items') export class ItemTypeormEntity { @PrimaryColumn('uuid') id: string; @Column() name: string; @Column({ type: 'timestamp with time zone' }) expirationDate: Date; @Column() orderUrl: string; @Column('uuid') userId: string; @Column({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' }) createdAt: Date; // Static factory methods for domain mapping static fromDomain(item: ItemAggregate): ItemTypeormEntity { const entity = new ItemTypeormEntity(); entity.id = item.id.value; entity.name = item.name; entity.expirationDate = item.expirationDate.value.toJSDate(); entity.orderUrl = item.orderUrl.value; entity.userId = item.userId.value; entity.createdAt = item.createdAt.toJSDate(); return entity; } toDomain(): ItemAggregate { return new ItemAggregate( new ItemId(this.id), this.name, new ExpirationDate(this.expirationDate), new OrderUrl(this.orderUrl), new UserId(this.userId) ); } } ``` **File**: `src/infrastructure/persistence/repositories/typeorm-item.repository.ts` **Purpose**: TypeORM implementation of item repository ```typescript @Injectable() export class TypeormItemRepository implements IItemRepository { constructor( @InjectRepository(ItemTypeormEntity) private readonly repository: Repository, private readonly logger: Logger ) {} async save(item: ItemAggregate): Promise { try { const entity = ItemTypeormEntity.fromDomain(item); await this.repository.save(entity); this.logger.log(`Item saved: ${item.id.value}`); } catch (error) { this.logger.error(`Failed to save item: ${error.message}`); throw new RepositoryException('Failed to save item', error); } } async findById(id: ItemId): Promise { try { const entity = await this.repository.findOne({ where: { id: id.value } }); return entity ? entity.toDomain() : null; } catch (error) { this.logger.error(`Failed to find item by ID: ${error.message}`); throw new RepositoryException('Failed to find item', error); } } async findByUserId(userId: UserId): Promise { try { const entities = await this.repository.find({ where: { userId: userId.value } }); return entities.map(entity => entity.toDomain()); } catch (error) { this.logger.error(`Failed to find items by user ID: ${error.message}`); throw new RepositoryException('Failed to find items', error); } } async findWhere(specification: Specification): Promise { try { const criteria = (specification as ItemExpirationSpecification).toCriteria(); const entities = await this.repository.find({ where: criteria }); return entities.map(entity => entity.toDomain()); } catch (error) { this.logger.error(`Failed to find items by specification: ${error.message}`); throw new RepositoryException('Failed to find items', error); } } async delete(id: ItemId): Promise { try { await this.repository.delete({ id: id.value }); this.logger.log(`Item deleted: ${id.value}`); } catch (error) { this.logger.error(`Failed to delete item: ${error.message}`); throw new RepositoryException('Failed to delete item', error); } } async exists(id: ItemId): Promise { try { const count = await this.repository.count({ where: { id: id.value } }); return count > 0; } catch (error) { this.logger.error(`Failed to check item existence: ${error.message}`); throw new RepositoryException('Failed to check item existence', error); } } } ``` #### 3.2 External Services **File**: `src/infrastructure/external/http-order.service.ts` **Purpose**: HTTP client for ordering items ```typescript @Injectable() export class HttpOrderService implements IOrderService { constructor( private readonly httpService: HttpService, private readonly configService: ConfigService, private readonly logger: Logger ) {} async orderItem(orderRequest: OrderRequest): Promise { try { const payload = { itemId: orderRequest.itemId.value, itemName: orderRequest.itemName, userId: orderRequest.userId.value, expirationDate: orderRequest.expirationDate.toISOString(), orderTimestamp: orderRequest.orderTimestamp.toISOString() }; const response = await firstValueFrom( this.httpService.post(orderRequest.orderUrl.value, payload, { timeout: this.configService.get('order.timeout', 30000), headers: { 'Content-Type': 'application/json', 'User-Agent': 'AutoStore/1.0', 'X-Request-ID': uuidv4() } }).pipe( catchError(error => { throw new OrderException( `Failed to order item: ${error.message}`, orderRequest.orderUrl.value ); }) ) ); if (response.status >= 400) { throw new OrderException( `Order failed with status ${response.status}`, orderRequest.orderUrl.value ); } this.logger.log(`Order placed successfully for item ${orderRequest.itemId.value}`); } catch (error) { this.logger.error(`Failed to place order: ${error.message}`); throw error; } } } ``` #### 3.3 Authentication **File**: `src/infrastructure/auth/jwt.strategy.ts` **Purpose**: JWT authentication strategy ```typescript @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private readonly configService: ConfigService, private readonly userRepository: IUserRepository ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get('jwt.secret'), }); } async validate(payload: JwtPayload): Promise { const user = await this.userRepository.findById(new UserId(payload.sub)); if (!user) { throw new UnauthorizedException('Invalid token'); } return { userId: user.id.value, username: user.username }; } } ``` ### 4. Presentation Layer (API) #### 4.1 Controllers **File**: `src/presentation/http/controllers/items.controller.ts` **Purpose**: REST API controller for items ```typescript @ApiTags('items') @Controller('api/v1/items') @UseGuards(JwtAuthGuard) @ApiBearerAuth() export class ItemsController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, private readonly logger: Logger ) {} @Post() @ApiOperation({ summary: 'Create a new item' }) @ApiResponse({ status: 201, description: 'Item created successfully' }) @ApiResponse({ status: 400, description: 'Invalid input data' }) async createItem( @Body() createItemDto: CreateItemRequestDto, @Req() request: Request ): Promise> { try { const user = request.user as UserPayload; const command = new AddItemCommand( createItemDto.name, createItemDto.expirationDate, createItemDto.orderUrl, user.userId ); const itemId = await this.commandBus.execute(command); return itemId ? new ApiResponse(true, 'Item created successfully', itemId) : new ApiResponse(true, 'Item expired and ordered successfully', null); } catch (error) { this.logger.error(`Failed to create item: ${error.message}`); throw new HttpException( new ApiResponse(false, error.message, null), HttpStatus.BAD_REQUEST ); } } @Get(':id') @ApiOperation({ summary: 'Get item by ID' }) @ApiResponse({ status: 200, description: 'Item retrieved successfully' }) @ApiResponse({ status: 404, description: 'Item not found' }) async getItem( @Param('id') itemId: string, @Req() request: Request ): Promise> { try { const user = request.user as UserPayload; const query = new GetItemQuery(itemId, user.userId); const item = await this.queryBus.execute(query); return new ApiResponse(true, 'Item retrieved successfully', item); } catch (error) { this.logger.error(`Failed to get item: ${error.message}`); throw new HttpException( new ApiResponse(false, error.message, null), error instanceof ItemNotFoundException ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST ); } } @Get() @ApiOperation({ summary: 'Get all user items' }) @ApiResponse({ status: 200, description: 'Items retrieved successfully' }) async listItems(@Req() request: Request): Promise> { try { const user = request.user as UserPayload; const query = new ListItemsQuery(user.userId); const items = await this.queryBus.execute(query); return new ApiResponse(true, 'Items retrieved successfully', items); } catch (error) { this.logger.error(`Failed to list items: ${error.message}`); throw new HttpException( new ApiResponse(false, error.message, null), HttpStatus.BAD_REQUEST ); } } @Delete(':id') @ApiOperation({ summary: 'Delete item' }) @ApiResponse({ status: 204, description: 'Item deleted successfully' }) @ApiResponse({ status: 404, description: 'Item not found' }) @HttpCode(HttpStatus.NO_CONTENT) async deleteItem( @Param('id') itemId: string, @Req() request: Request ): Promise { try { const user = request.user as UserPayload; const command = new DeleteItemCommand(itemId, user.userId); await this.commandBus.execute(command); } catch (error) { this.logger.error(`Failed to delete item: ${error.message}`); throw new HttpException( new ApiResponse(false, error.message, null), error instanceof ItemNotFoundException ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST ); } } } ``` #### 4.2 Exception Handling **File**: `src/presentation/http/filters/exception.filter.ts` **Purpose**: Global exception filter for consistent error responses ```typescript @Catch() export class GlobalExceptionFilter implements ExceptionFilter { constructor( private readonly logger: Logger ) {} catch(exception: unknown, host: ArgumentsHost): void { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); let status: number; let message: string; let error: string; if (exception instanceof HttpException) { status = exception.getStatus(); const exceptionResponse = exception.getResponse() as any; message = exceptionResponse.message || exception.message; error = exceptionResponse.error || 'Http Exception'; } else if (exception instanceof DomainException) { status = HttpStatus.BAD_REQUEST; message = exception.message; error = 'Domain Exception'; } else { status = HttpStatus.INTERNAL_SERVER_ERROR; message = 'Internal server error'; error = 'Internal Server Error'; } this.logger.error( `${request.method} ${request.url} - ${status} - ${message}`, exception instanceof Error ? exception.stack : undefined ); response.status(status).json({ success: false, message, error, timestamp: new Date().toISOString(), path: request.url }); } } ``` ### 5. Cross-Cutting Concerns #### 5.1 Event Handlers **File**: `src/infrastructure/events/handlers/item-expired.handler.ts` **Purpose**: Event handler for item expiration ```typescript @EventsHandler(ItemExpiredEvent) export class ItemExpiredHandler implements IEventHandler { constructor( private readonly orderService: IOrderService, private readonly itemRepository: IItemRepository, private readonly logger: Logger ) {} async handle(event: ItemExpiredEvent): Promise { try { this.logger.log(`Processing expired item: ${event.itemId.value}`); const orderRequest = new OrderRequest( event.itemId, event.itemName, event.userId, event.orderUrl, DateTime.now() ); await this.orderService.orderItem(orderRequest); await this.itemRepository.delete(event.itemId); this.logger.log(`Expired item processed successfully: ${event.itemId.value}`); } catch (error) { this.logger.error(`Failed to process expired item ${event.itemId.value}: ${error.message}`); // Implement retry logic or dead letter queue } } } ``` #### 5.2 Scheduling **File**: `src/infrastructure/schedulers/expired-items.scheduler.ts` **Purpose**: Scheduled job for processing expired items ```typescript @Injectable() export class ExpiredItemsScheduler { constructor( private readonly itemRepository: IItemRepository, private readonly expirationDomainService: ItemExpirationDomainService, private readonly eventBus: IEventBus, private readonly logger: Logger ) {} @Cron('0 * * * * *') // Every minute async handleExpiredItems(): Promise { try { this.logger.log('Starting expired items processing'); const currentTime = this.timeProvider.now(); const specification = this.expirationDomainService.getExpiredItemsSpecification(currentTime); const expiredItems = await this.itemRepository.findWhere(specification); for (const item of expiredItems) { const expirationResult = this.expirationDomainService.processItemExpiration(item); if (expirationResult.isExpired) { await this.eventBus.publish(item.pullDomainEvents()); } } this.logger.log(`Processed ${expiredItems.length} expired items`); } catch (error) { this.logger.error(`Failed to process expired items: ${error.message}`); } } } ``` ### 6. Module Organization **File**: `src/infrastructure/modules/item.module.ts` **Purpose**: Item feature module with proper dependency injection ```typescript @Module({ imports: [ TypeOrmModule.forFeature([ItemTypeormEntity]), CqrsModule, HttpModule, ], controllers: [ItemsController], providers: [ // Domain Services ItemExpirationDomainService, // Repositories { provide: IItemRepository, useClass: TypeormItemRepository }, // External Services { provide: IOrderService, useClass: HttpOrderService }, // Command Handlers AddItemCommandHandler, DeleteItemCommandHandler, // Query Handlers GetItemQueryHandler, ListItemsQueryHandler, // Event Handlers ItemExpiredHandler, // Schedulers ExpiredItemsScheduler, // Value Objects (as providers for dependency injection) { provide: 'ITimeProvider', useClass: SystemTimeProvider } ], exports: [ IItemRepository, ItemExpirationDomainService ] }) export class ItemModule {} ``` ### 7. Testing Strategy #### 7.1 Unit Tests **File**: `tests/unit/domain/aggregates/item.aggregate.spec.ts` **Purpose**: Unit tests for Item aggregate ```typescript describe('ItemAggregate', () => { let item: ItemAggregate; let itemId: ItemId; let expirationDate: ExpirationDate; let orderUrl: OrderUrl; let userId: UserId; beforeEach(() => { itemId = new ItemId(uuidv4()); expirationDate = new ExpirationDate(DateTime.now().plus({ days: 1 })); orderUrl = new OrderUrl('https://example.com/order'); userId = new UserId(uuidv4()); item = new ItemAggregate(itemId, 'Test Item', expirationDate, orderUrl, userId); }); describe('constructor', () => { it('should create a valid item', () => { expect(item.id).toEqual(itemId); expect(item.name).toBe('Test Item'); expect(item.expirationDate).toEqual(expirationDate); expect(item.orderUrl).toEqual(orderUrl); expect(item.userId).toEqual(userId); }); it('should throw error for empty name', () => { expect(() => new ItemAggregate(itemId, '', expirationDate, orderUrl, userId)) .toThrow(InvalidItemDataException); }); }); describe('checkExpiration', () => { it('should return true for expired item', () => { const pastDate = new ExpirationDate(DateTime.now().minus({ days: 1 })); const expiredItem = new ItemAggregate(itemId, 'Expired Item', pastDate, orderUrl, userId); const currentTime = DateTime.now(); const result = expiredItem.checkExpiration(currentTime); expect(result).toBe(true); expect(expiredItem.pullDomainEvents()).toHaveLength(1); }); it('should return false for non-expired item', () => { const currentTime = DateTime.now(); const result = item.checkExpiration(currentTime); expect(result).toBe(false); expect(item.pullDomainEvents()).toHaveLength(0); }); }); }); ``` #### 7.2 Integration Tests **File**: `tests/integration/commands/add-item.command.integration.spec.ts` **Purpose**: Integration tests for AddItem command ```typescript describe('AddItemCommand Integration', () => { let commandBus: CommandBus; let eventBus: EventBus; let itemRepository: IItemRepository; let module: TestingModule; beforeEach(async () => { module = await Test.createTestingModule({ imports: [ CqrsModule, TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [ItemTypeormEntity], synchronize: true, }), ItemModule, ], }).compile(); commandBus = module.get(CommandBus); eventBus = module.get(EventBus); itemRepository = module.get(IItemRepository); }); it('should create a new item', async () => { const command = new AddItemCommand( 'Test Item', DateTime.now().plus({ days: 1 }).toISO(), 'https://example.com/order', uuidv4() ); const itemId = await commandBus.execute(command); expect(itemId).toBeDefined(); const savedItem = await itemRepository.findById(new ItemId(itemId)); expect(savedItem).toBeDefined(); expect(savedItem.name).toBe('Test Item'); }); it('should handle expired item immediately', async () => { const command = new AddItemCommand( 'Expired Item', DateTime.now().minus({ days: 1 }).toISO(), 'https://example.com/order', uuidv4() ); const itemId = await commandBus.execute(command); expect(itemId).toBeNull(); const savedItem = await itemRepository.findById(new ItemId('any-id')); expect(savedItem).toBeNull(); }); }); ``` ### 8. Configuration **File**: `src/config/configuration.ts` **Purpose**: Application configuration ```typescript export default () => ({ app: { name: 'AutoStore', version: '1.0.0', port: process.env.PORT || 3000, }, database: { type: 'postgres', host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT, 10) || 5432, username: process.env.DB_USERNAME || 'autostore', password: process.env.DB_PASSWORD || 'password', database: process.env.DB_DATABASE || 'autostore', }, jwt: { secret: process.env.JWT_SECRET || 'your-secret-key', expiresIn: process.env.JWT_EXPIRES_IN || '1d', }, order: { timeout: parseInt(process.env.ORDER_TIMEOUT, 10) || 30000, retryAttempts: parseInt(process.env.ORDER_RETRY_ATTEMPTS, 10) || 3, }, logging: { level: process.env.LOG_LEVEL || 'info', }, }); ``` ## Implementation Roadmap ### Phase 1: Core Domain (Week 1) 1. **Domain Layer**: Implement aggregates, value objects, domain services, and events 2. **Unit Tests**: Comprehensive unit tests for all domain components 3. **Domain Specifications**: Implement specification pattern for complex queries ### Phase 2: Application Layer (Week 2) 1. **CQRS Setup**: Implement command and query buses 2. **Use Cases**: Implement all commands and queries with proper handlers 3. **DTOs**: Create request and response DTOs with validation 4. **Application Services**: Implement domain services for complex business logic ### Phase 3: Infrastructure Layer (Week 3) 1. **Persistence**: Implement TypeORM repositories and entities 2. **External Services**: Implement HTTP client for ordering 3. **Authentication**: Implement JWT authentication and authorization 4. **Configuration**: Set up configuration management ### Phase 4: Presentation Layer (Week 4) 1. **Controllers**: Implement REST API controllers 2. **Exception Handling**: Implement global exception filters 3. **API Documentation**: Add Swagger/OpenAPI documentation 4. **Validation**: Implement request validation and transformations ### Phase 5: Cross-Cutting Concerns (Week 5) 1. **Event Handling**: Implement domain event handlers 2. **Scheduling**: Implement background job for expired items 3. **Logging**: Implement structured logging throughout the application 4. **Monitoring**: Add health checks and metrics ### Phase 6: Testing and Deployment (Week 6) 1. **Integration Tests**: Comprehensive integration tests 2. **E2E Tests**: End-to-end testing with real HTTP requests 3. **Docker**: Containerize the application 4. **Documentation**: Complete documentation and README ## Best Practices and Patterns ### 1. SOLID Principles - **Single Responsibility**: Each class has one reason to change - **Open/Closed**: Open for extension, closed for modification - **Liskov Substitution**: Subtypes must be substitutable for base types - **Interface Segregation**: Client-specific interfaces - **Dependency Inversion**: Depend on abstractions, not concretions ### 2. DDD Patterns - **Aggregates**: Consistency boundaries and transactional units - **Value Objects**: Immutable objects with equality based on values - **Domain Services**: Business logic that doesn't fit in entities - **Domain Events**: Decoupled communication through events - **Repositories**: Collection-like access to aggregates ### 3. Clean Architecture - **Dependency Rule**: Dependencies point inward - **Framework Independence**: Business logic doesn't depend on frameworks - **Testability**: Easy to test business logic in isolation - **UI Independence**: Business logic doesn't depend on UI ### 4. NestJS Specific Patterns - **Modules**: Organize related functionality - **Dependency Injection**: Leverage NestJS IoC container - **Decorators**: Use decorators for metadata and configuration - **Interceptors**: Implement cross-cutting concerns - **Guards**: Implement authentication and authorization ## Development Guidelines ### 1. Code Quality - Use TypeScript strict mode - Implement ESLint and Prettier - Use meaningful variable and function names - Keep functions small and focused - Write DRY (Don't Repeat Yourself) code ### 2. Error Handling - Use custom exception classes - Implement proper error logging - Provide meaningful error messages - Handle both domain and technical errors - Implement graceful degradation ### 3. Performance Considerations - Implement database connection pooling - Use caching for frequently accessed data - Implement pagination for large datasets - Optimize database queries - Implement request/response compression ### 4. Security Considerations - Validate all input data - Implement proper authentication and authorization - Use HTTPS for all communications - Implement rate limiting - Sanitize all user input This comprehensive implementation plan ensures consistent, enterprise-grade development of the AutoStore system using NestJS with TypeScript, following DDD and Clean Architecture principles. The plan provides detailed guidance for developers at all levels while maintaining flexibility for implementation choices.