Skip to main content

Coding Standards

Coding conventions for the CRM platform.

TypeScript

Naming Conventions

TypeConventionExample
ClassesPascalCaseCustomerProfileService
InterfacesPascalCase, prefix with I (optional)CustomerProfile
EnumsPascalCaseCustomerStatus
FunctionscamelCaseresolveIdentity()
VariablescamelCasecustomerProfile
ConstantsSCREAMING_SNAKE_CASEMAX_RETRY_COUNT
Fileskebab-casecustomer-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.ts next 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);