Coding Standards
Coding conventions for the CRM platform.
TypeScript
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Classes | PascalCase | CustomerProfileService |
| Interfaces | PascalCase, prefix with I (optional) | CustomerProfile |
| Enums | PascalCase | CustomerStatus |
| Functions | camelCase | resolveIdentity() |
| Variables | camelCase | customerProfile |
| Constants | SCREAMING_SNAKE_CASE | MAX_RETRY_COUNT |
| Files | kebab-case | customer-profile.service.ts |
File Organization
// 1. Imports (external first, then internal)
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/crm';
import { CustomerProfile } from './types';
import { normalizeEmail } from './utils';
// 2. Constants
const MAX_BATCH_SIZE = 100;
// 3. Types/Interfaces (if not in separate file)
interface CreateProfileInput { ... }
// 4. Class definition
@Injectable()
export class CustomerProfileService {
// 4a. Private fields
private readonly logger = new Logger(CustomerProfileService.name);
// 4b. Constructor
constructor(private readonly prisma: PrismaClient) {}
// 4c. Public methods
async create(input: CreateProfileInput): Promise<CustomerProfile> { ... }
// 4d. Private methods
private validateInput(input: CreateProfileInput): void { ... }
}
Error Handling
// Use custom exceptions
throw new NotFoundException(`Customer ${id} not found`);
throw new ConflictException('Email already in use');
// Don't catch and ignore errors
// Bad:
try {
await this.send();
} catch (e) {
// silently ignore
}
// Good:
try {
await this.send();
} catch (error) {
this.logger.error('Failed to send', error);
throw new InternalServerErrorException('Send failed');
}
Async/Await
// Always use async/await over raw promises
// Good:
const profile = await this.prisma.customerProfile.findUnique({ where: { id } });
// Avoid:
this.prisma.customerProfile.findUnique({ where: { id } }).then(profile => { ... });
NestJS Patterns
Services
- One service per domain entity
- Inject dependencies via constructor
- Use DTOs for input validation
@Injectable()
export class CustomerProfileService {
constructor(
private readonly prisma: PrismaClient,
private readonly eventEmitter: EventEmitter2,
) {}
async create(dto: CreateProfileDto): Promise<CustomerProfile> {
const profile = await this.prisma.customerProfile.create({
data: dto,
});
this.eventEmitter.emit('profile.created', profile);
return profile;
}
}
Controllers
- Thin controllers, business logic in services
- Use decorators for validation and auth
- Return consistent response shapes
@Controller('customers')
@UseGuards(JwtAuthGuard)
export class CustomerController {
constructor(private readonly service: CustomerProfileService) {}
@Post()
async create(@Body() dto: CreateProfileDto): Promise<ApiResponse<CustomerProfile>> {
const profile = await this.service.create(dto);
return { data: profile };
}
}
DTOs
- Use class-validator decorators
- Separate Create and Update DTOs
- Use PartialType for updates
export class CreateProfileDto {
@IsString()
tenantId: string;
@IsEmail()
@IsOptional()
email?: string;
@IsString()
@IsOptional()
firstName?: string;
}
export class UpdateProfileDto extends PartialType(CreateProfileDto) {}
Prisma
Queries
// Always include tenantId in queries
const profile = await this.prisma.customerProfile.findFirst({
where: {
id,
tenantId, // Required for multi-tenancy
},
});
// Use select for partial data
const emails = await this.prisma.customerProfile.findMany({
where: { tenantId },
select: { id: true, email: true },
});
// Use include for relations
const profile = await this.prisma.customerProfile.findUnique({
where: { id },
include: { activities: true },
});
Transactions
// Use transactions for multi-step operations
await this.prisma.$transaction(async (tx) => {
const profile = await tx.customerProfile.create({ data });
await tx.activity.create({ data: { customerId: profile.id } });
});
Testing
Test File Naming
- Unit tests:
*.spec.tsnext to source - Integration tests:
*.integration.spec.ts - E2E tests:
*.e2e-spec.ts
Test Structure
describe('CustomerProfileService', () => {
let service: CustomerProfileService;
let mockPrisma: DeepMockProxy<PrismaClient>;
beforeEach(() => {
mockPrisma = mockDeep<PrismaClient>();
service = new CustomerProfileService(mockPrisma);
});
describe('create', () => {
it('should create a new profile', async () => {
// Arrange
const dto = { tenantId: 't1', email: 'test@example.com' };
mockPrisma.customerProfile.create.mockResolvedValue({ id: '1', ...dto });
// Act
const result = await service.create(dto);
// Assert
expect(result.email).toBe('test@example.com');
expect(mockPrisma.customerProfile.create).toHaveBeenCalledWith({
data: dto,
});
});
});
});
Comments
// Use JSDoc for public APIs
/**
* Resolves a customer identity from the given identifiers.
* @param identifiers - Identity keys to match
* @returns The matched customer profile or null
*/
async resolve(identifiers: IdentityKeys): Promise<CustomerProfile | null> { ... }
// Use // for inline explanations
// E.164 format required for phone matching
const normalizedPhone = normalizeToE164(phone);