Skip to main content

Code Patterns & Architecture

This document outlines the coding patterns, architectural decisions, and best practices used in the Comdeall backend application. These patterns ensure consistency, maintainability, and scalability across the codebase.


๐Ÿ—๏ธ Architecture Overviewโ€‹

Layered Architectureโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ API Layer โ”‚ โ† Controllers, DTOs, Services
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Business Logic โ”‚ โ† Domain Services, Validation
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Data Access Layer โ”‚ โ† Repositories, DB Services
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Infrastructure โ”‚ โ† Database, Redis, External APIs
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Module Structureโ€‹

Each module in the Comdeall backend follows a consistent structure across the codebase. The modules are divided across two main directories: src/api for the API layer and src/db for the database layer.

src/
โ”œโ”€โ”€ api/ # API Controllers & Services
โ”‚ โ”œโ”€โ”€ auth/ # Authentication & Authorization
โ”‚ โ”œโ”€โ”€ user/ # User Management (Parents, Therapists)
โ”‚ โ”œโ”€โ”€ assessment/ # Child Assessment Management
โ”‚ โ”œโ”€โ”€ lesson-plan/ # Lesson Plan Management
โ”‚ โ”œโ”€โ”€ appointments/ # Appointment Scheduling
โ”‚ โ”œโ”€โ”€ payment/ # Payment Processing
โ”‚ โ”œโ”€โ”€ subscription/ # Subscription Management
โ”‚ โ”œโ”€โ”€ notifications/ # Push Notifications
โ”‚ โ”œโ”€โ”€ support/ # Support Ticket System
โ”‚ โ””โ”€โ”€ media/ # File Upload/Management
โ”œโ”€โ”€ db/ # Data Access Layer
โ”‚ โ”œโ”€โ”€ user/ # User Repository & DB Service
โ”‚ โ”œโ”€โ”€ assessment/ # Assessment Repository & DB Service
โ”‚ โ”œโ”€โ”€ lesson-plan/ # Lesson Plan Repository & DB Service
โ”‚ โ””โ”€โ”€ prisma/ # Database Schema
โ”œโ”€โ”€ common/ # Shared Utilities
โ”‚ โ”œโ”€โ”€ dto/ # Common DTOs
โ”‚ โ”œโ”€โ”€ enum/ # Shared Enums
โ”‚ โ”œโ”€โ”€ helpers/ # Utility Functions
โ”‚ โ””โ”€โ”€ services/ # Common Services
โ”œโ”€โ”€ background/ # Background Jobs & Queues
โ”‚ โ”œโ”€โ”€ queue/ # Queue Services & Processors
โ”‚ โ””โ”€โ”€ cron/ # Scheduled Jobs
โ”œโ”€โ”€ interceptors/ # Request/Response Interceptors
โ”œโ”€โ”€ middlewares/ # Custom Middlewares
โ””โ”€โ”€ socket/ # WebSocket Implementation

API Layer Structureโ€‹

The API layer in src/api/module-name/ follows this structure:

module-name/
โ”œโ”€โ”€ dto/
โ”‚ โ”œโ”€โ”€ create-*.dto.ts
โ”‚ โ”œโ”€โ”€ update-*.dto.ts
โ”‚ โ”œโ”€โ”€ *-response.dto.ts
โ”‚ โ””โ”€โ”€ [other DTOs]
โ”œโ”€โ”€ enum/
โ”‚ โ”œโ”€โ”€ *-status.enum.ts
โ”‚ โ”œโ”€โ”€ *-type.enum.ts
โ”‚ โ””โ”€โ”€ [other enums]
โ”œโ”€โ”€ helpers/
โ”‚ โ””โ”€โ”€ *-helper.ts
โ”œโ”€โ”€ module-name.controller.ts
โ”œโ”€โ”€ module-name.module.ts
โ”œโ”€โ”€ module-name.service.ts
โ””โ”€โ”€ module-name.transform.ts

Database Layer Structureโ€‹

The corresponding database layer in src/db/module-name/ follows this structure:

module-name/
โ”œโ”€โ”€ module-name.repository.ts
โ””โ”€โ”€ module-name-db.service.ts

๐Ÿ“ Naming Conventionsโ€‹

Files & Classesโ€‹

// Files: kebab-case
user.service.ts, create-parent.dto.ts

// Classes: PascalCase
export class UserService { }
export class CreateParentDto { }

// Enums: PascalCase with descriptive values
export enum UserRole {
ADMIN = "ADMIN",
PARENT = "PARENT",
THERAPIST = "THERAPIST"
}

๐Ÿ”ง Service Layer Patternsโ€‹

API Service Structureโ€‹

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

constructor(
private readonly userDBService: UserDBService,
private readonly configService: ConfigService<EnvConfig>
) {}

async createParent(parentData: CreateParentDto): Promise<InternalResponse> {
// 1. Validate business rules
// 2. Call DB service
// 3. Queue background jobs
// 4. Return standardized response
}
}

Key Points:

  • Services handle business logic
  • Use dependency injection for DB services
  • Always return InternalResponse format
  • Log important operations

๐Ÿ“‹ DTO Patternsโ€‹

Request DTOs with Validationโ€‹

export class CreateParentDto {
@ApiProperty({ description: 'Parent name', example: 'John Doe' })
@IsString()
@IsNotEmpty()
@Length(3, 30)
@Matches(REGEX.NAME)
parent_name: string;

@IsEmail()
email: string;
}

Key Points:

  • Use @ApiProperty for Swagger documentation
  • Validate all inputs with class-validator decorators
  • Use custom regex patterns for specific formats

Response Wrapperโ€‹

export class CreateParentInput {
@ValidateNested()
@Type(() => CreateParentObject)
input: CreateParentObject;
}

Key Points:

  • Wrap complex DTOs in input objects
  • Use @ValidateNested() for nested validation
  • Use @Type() for proper transformation

๐ŸŽฎ Controller Patternsโ€‹

Standard Controllerโ€‹

@Controller('user-management')
@ApiTags('User Management')
@UseGuards(JwtAuthGuard)
export class UserController {
@Post('create-parent')
@ApiOperation({ summary: 'Create parent account' })
@ApiResponse({ status: 201, type: UserResponseDto })
async createParent(@Body() dto: CreateParentDto): Promise<InternalResponse> {
return this.userService.createParent(dto);
}
}

Key Points:

  • Use route names from RouteNames enum
  • Add comprehensive Swagger documentation
  • Apply appropriate guards (JWT, Roles)
  • Keep controllers thin - delegate to services

๐Ÿ”„ Background Job Patternsโ€‹

Queue Serviceโ€‹

@Injectable()
export class EmailQueueService {
async sendOtpEmail(data: IOtpEmailJob): Promise<void> {
// Process email sending logic
await this.emailService.sendEmail(emailData);
}
}

Job Interfaceโ€‹

export interface IOtpEmailJob {
email: string;
otp: string;
}

Key Points:

  • Define clear interfaces for job data
  • Use descriptive job names from JobName enum
  • Handle errors gracefully in processors
  • Log job processing for monitoring

๐Ÿ›ก๏ธ Error Handlingโ€‹

Service Levelโ€‹

async createUser(userData: CreateUserDto): Promise<InternalResponse> {
try {
// Business logic
return { success: true, data: result };
} catch (error) {
this.logger.error(`Error: ${error.message}`, error.stack);
throw new InternalServerErrorException('Operation failed');
}
}

Key Points:

  • Always wrap in try-catch
  • Log errors with context
  • Throw appropriate HTTP exceptions
  • Return consistent response format

๐Ÿ” Validation Patternsโ€‹

Custom Validatorsโ€‹

@IsValidPhoneNumber({ message: 'Invalid phone format' })
contact: string;

Regex Constantsโ€‹

export const REGEX = {
NAME: /^[a-zA-Z\s]+$/,
PHONE: /^\+[1-9]\d{1,14}$/,
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
};

Key Points:

  • Create reusable regex patterns
  • Use custom validators for complex validation
  • Provide clear error messages

๐Ÿ”„ Interceptor Patternsโ€‹

Response Transformationโ€‹

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
success: data?.success,
message: data?.message,
data: data?.data || data
}))
);
}
}

Key Points:

  • Standardize all API responses
  • Exclude health/metrics endpoints
  • Handle both HTTP and GraphQL contexts

๐Ÿ—„๏ธ Database Patternsโ€‹

Repository Patternโ€‹

@Injectable()
export class UserRepository {
constructor(private readonly prisma: DBService) {}

async getUserById(id: string) {
return this.prisma.user.findUnique({
where: { id },
include: { parent: { include: { children: true } } }
});
}
}

DB Service Layerโ€‹

@Injectable()
export class UserDBService {
constructor(private readonly userRepository: UserRepository) {}

async getUserById(id: string) {
return this.userRepository.getUserById(id);
}
}

Key Points:

  • Repository handles raw database operations
  • DB Service provides business-focused methods
  • Use proper includes for related data
  • Keep queries optimized with select/where clauses

๐Ÿ” Authentication Patternsโ€‹

JWT Guard Usageโ€‹

@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.THERAPIST)
@Get('admin-only')
async adminEndpoint() { }

Key Points:

  • Use JWT guard for authentication
  • Use Roles guard for authorization
  • Apply guards at controller or method level

๐Ÿ“Š Logging & Monitoringโ€‹

Structured Loggingโ€‹

private readonly logger = new Logger(ServiceName.name);

this.logger.log('Operation completed successfully');
this.logger.error('Operation failed', error.stack);
this.logger.debug('Debug information', context);

Key Points:

  • Use NestJS Logger with service name
  • Log at appropriate levels (log, error, debug, warn)
  • Include context and stack traces for errors

๐Ÿงช Testing Patternsโ€‹

Unit Test Structureโ€‹

describe('UserService', () => {
let service: UserService;
let userDBService: UserDBService;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UserService,
{ provide: UserDBService, useValue: mockUserDBService }
]
}).compile();

service = module.get<UserService>(UserService);
});

it('should create user successfully', async () => {
// Arrange
const userData = { name: 'John', email: 'john@test.com' };
jest.spyOn(userDBService, 'createUser').mockResolvedValue(mockUser);

// Act
const result = await service.createUser(userData);

// Assert
expect(result.success).toBe(true);
expect(userDBService.createUser).toHaveBeenCalledWith(userData);
});
});

Key Points:

  • Use AAA pattern (Arrange, Act, Assert)
  • Mock dependencies properly
  • Test both success and error scenarios
  • Use descriptive test names

๐Ÿš€ Performance Best Practicesโ€‹

Cachingโ€‹

@Cacheable('user', 300) // 5 minutes
async getUserById(id: string) {
return this.userDBService.getUserById(id);
}

Database Optimizationโ€‹

// Use select to limit fields
async getUsersList() {
return this.prisma.user.findMany({
select: { id: true, email: true, role: true },
take: 100
});
}

Key Points:

  • Cache frequently accessed data
  • Use select to limit returned fields
  • Implement pagination for large datasets
  • Use proper database indexes

๐Ÿ“‹ Quick Referenceโ€‹

โœ… Do'sโ€‹

  • Use consistent naming conventions
  • Implement proper error handling
  • Validate all inputs
  • Use dependency injection
  • Follow repository pattern
  • Log important operations
  • Write comprehensive tests

โŒ Don'tsโ€‹

  • Don't expose database models directly
  • Don't use any type
  • Don't ignore error handling
  • Don't hardcode configuration
  • Don't skip input validation
  • Don't use console.log in production

This guide provides the essential patterns used in Comdeall backend. Follow these patterns for consistency and maintainability across the codebase.