Coding Standards
Best practices and conventions for maintaining consistent, high-quality code across the 2KRIKA codebase.
Overview
- TypeScript strict mode for type safety
- ESLint for code quality
- Prettier for code formatting
- Clean Architecture principles
- SOLID design patterns
- Consistent naming conventions
TypeScript Configuration
Strict Mode
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "es2023",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
Path Aliases
Always use @/ path aliases for imports:
// ✅ Correct
import { User } from '@/domain/user/user.entity';
import { UserRepository } from '@/domain/user/user.repository';
// ❌ Incorrect
import { User } from '../../../domain/user/user.entity';
import { UserRepository } from '../../../domain/user/user.repository';
ESLint Rules
Configuration
// eslint.config.mjs
export default [
{
files: ["**/*.ts"],
rules: {
"no-console": "warn",
"@typescript-eslint/no-explicit-any": "off",
"semi": ["error", "always"],
"quotes": ["error", "double"],
"prettier/prettier": "error",
},
},
];
Key Rules
Semicolons – Always required:
// ✅ Correct
const user = getUser();
await userRepository.save(user);
// ❌ Incorrect
const user = getUser()
await userRepository.save(user)
Quotes – Use double quotes:
Console Statements – Warning (remove before production):
// ⚠️ Warning - Use logger instead
console.log("Debug message");
// ✅ Correct
logger.info("User logged in");
Code Formatting
Prettier Configuration
Run formatting before commits:
Formatting Rules
Indentation – 2 spaces:
Line Length – 80-100 characters preferred:
// ✅ Good
const service = await serviceRepository.findOne({
id: serviceId
});
// ✅ Also good - wrap long parameters
const order = await orderRepository.create({
sellerId,
customerId,
serviceId,
price,
deliveryTime,
});
Naming Conventions
Files and Folders
Files – kebab-case:
Folders – lowercase:
Code Naming
Classes – PascalCase:
class User extends AggregateRoot {}
class ServiceRepository extends BaseRepository {}
class CreateOrderDTO {}
Interfaces – PascalCase with descriptive names:
Variables/Functions – camelCase:
const userId = "user-123";
const orderTotal = new Decimal(100);
function calculateTotal(items: Item[]) {}
async function getUserById(id: string) {}
Constants – UPPER_SNAKE_CASE:
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const DEFAULT_PAGE_SIZE = 20;
const ORDER_STATES = ['pending', 'accepted', 'completed'];
Private members – Prefix with underscore:
class OrderService {
private _repository: OrderRepository;
private _notifier: Notifier;
private _validateOrder(order: Order) {}
}
Clean Architecture Patterns
Layer Separation
Domain Layer – Pure business logic:
// ✅ Correct - Domain entity
export class Order extends AggregateRoot<OrderProps> {
accept(): void {
if (this.status !== 'pending') {
throw new InvalidStateError('Order must be pending to accept');
}
this.props.status = 'accepted';
}
}
// ❌ Incorrect - No framework dependencies in domain
import { Injectable } from '@nestjs/common'; // Don't do this!
Application Layer – Use cases orchestrate domain:
// ✅ Correct - Use case with dependencies
export async function acceptOrder(
orderId: string,
sellerId: string,
orderRepo: OrderRepository,
notifier: Notifier,
): Promise<Order> {
const order = await orderRepo.get(orderId);
if (order.sellerId !== sellerId) {
throw new ForbiddenError('Not authorized');
}
order.accept();
await orderRepo.update(order);
await notifier.notify('Order accepted', order.customer);
return order;
}
Adapters Layer – External integrations:
// ✅ Correct - Adapter implements domain interface
export class UserSqlRepository implements UserRepository {
async get(id: string): Promise<User> {
const model = await db.User.findByPk(id);
return this.loads(model);
}
}
Web Layer – HTTP/Framework specific:
// ✅ Correct - Controller delegates to use cases
@Controller('orders')
export class OrderController {
@Post(':id/accept')
async accept(@Param('id') id: string, @Request() request: AuthenticatedRequest) {
return acceptOrder(id, request.user, orderRepo, notifier);
}
}
Dependency Direction
Always point inward:
// ✅ Correct - Web depends on Application
import { createOrder } from '@/app/order/usecases';
// ❌ Incorrect - Application depends on Web
import { OrderController } from '@/web/orders/order.controller';
SOLID Principles
Single Responsibility
Each class/function has one job:
// ✅ Correct - Single responsibility
class OrderRepository {
async save(order: Order): Promise<void> {}
async get(id: string): Promise<Order> {}
}
class OrderNotifier {
async notifyOrderCreated(order: Order): Promise<void> {}
}
// ❌ Incorrect - Too many responsibilities
class OrderService {
async save(order: Order): Promise<void> {}
async sendEmail(order: Order): Promise<void> {}
async logActivity(order: Order): Promise<void> {}
}
Open/Closed
Open for extension, closed for modification:
// ✅ Correct - Abstract interface
export abstract class PaymentGateway {
abstract initialize(amount: Decimal): Promise<PaymentInitResponse>;
abstract verify(reference: string): Promise<PaymentVerifyResponse>;
}
// Extend without modifying
export class PaystackGateway extends PaymentGateway {
async initialize(amount: Decimal) {
// Paystack implementation
}
}
export class StripeGateway extends PaymentGateway {
async initialize(amount: Decimal) {
// Stripe implementation
}
}
Liskov Substitution
Subtypes must be substitutable:
// ✅ Correct - Can substitute any repository
function getEntity<T>(id: string, repo: AbstractRepository<T>): Promise<T> {
return repo.get(id);
}
// Works with any repository
const user = await getEntity('user-123', userRepository);
const order = await getEntity('order-456', orderRepository);
Interface Segregation
Don't force implementations of unused methods:
// ✅ Correct - Specific interfaces
interface Readable<T> {
get(id: string): Promise<T>;
findAll(): Promise<T[]>;
}
interface Writable<T> {
save(entity: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Implement only what's needed
class ReadOnlyRepository<T> implements Readable<T> {
async get(id: string): Promise<T> {}
async findAll(): Promise<T[]> {}
}
Dependency Inversion
Depend on abstractions, not concretions:
// ✅ Correct - Depend on abstract interface
export async function createOrder(
data: CreateOrderDTO,
orderRepo: OrderRepository, // Abstract interface
paymentGateway: PaymentGateway, // Abstract interface
): Promise<Order> {
// Implementation
}
// ❌ Incorrect - Depend on concrete implementation
export async function createOrder(
data: CreateOrderDTO,
orderRepo: OrderSqlRepository, // Concrete class
paymentGateway: PaystackGateway, // Concrete class
): Promise<Order> {
// Implementation
}
Error Handling
Custom Exceptions
Use domain-specific exceptions:
// ✅ Correct - Descriptive exceptions
throw new ValidationError('Email is required');
throw new ForbiddenError('User not authorized');
throw new NotFound('Order not found');
// ❌ Incorrect - Generic errors
throw new Error('Invalid');
throw new Error('Something went wrong');
Try-Catch Blocks
Handle errors appropriately:
// ✅ Correct - Specific error handling
try {
const payment = await paymentGateway.verify(reference);
return payment;
} catch (error) {
logger.error('Payment verification failed', error);
throw new PaymentVerificationError(
'Unable to verify payment. Please contact support.'
);
}
// ❌ Incorrect - Swallowing errors
try {
const payment = await paymentGateway.verify(reference);
return payment;
} catch (error) {
// Silent failure
}
Type Safety
Avoid any
Use proper types:
// ✅ Correct - Specific types
interface CreateOrderDTO {
serviceId: string;
quantity: number;
options: Option[];
}
function createOrder(data: CreateOrderDTO): Promise<Order> {}
// ❌ Incorrect - Using any
function createOrder(data: any): Promise<any> {}
Use Type Guards
// ✅ Correct - Type guard
function isPaymentCompleted(payment: Payment): boolean {
return payment.status === 'completed';
}
if (isPaymentCompleted(payment)) {
// TypeScript knows payment is completed
}
Discriminated Unions
// ✅ Correct - Discriminated union
type OrderStatus =
| { state: 'pending'; paymentId: null }
| { state: 'paid'; paymentId: string }
| { state: 'completed'; deliveredAt: Date };
function handleOrder(status: OrderStatus) {
switch (status.state) {
case 'pending':
// status.paymentId is null
break;
case 'paid':
// status.paymentId is string
break;
case 'completed':
// status.deliveredAt is Date
break;
}
}
Comments and Documentation
JSDoc for Public APIs
/**
* Creates a new order for a service.
*
* @param data - The order creation data
* @param sellerId - ID of the service seller
* @param customerId - ID of the customer
* @param orderRepo - Order repository
* @returns The created order
* @throws {ValidationError} If order data is invalid
* @throws {ForbiddenError} If user not authorized
*/
export async function createOrder(
data: CreateOrderDTO,
sellerId: string,
customerId: string,
orderRepo: OrderRepository,
): Promise<Order> {
// Implementation
}
Inline Comments
Explain why, not what:
// ✅ Correct - Explains reasoning
// Reserve balance to prevent concurrent withdrawal attempts
await account.reserve(amount);
// ❌ Incorrect - States the obvious
// Call the reserve method with amount
await account.reserve(amount);
TODO Comments
Track pending work:
// TODO: Implement retry logic for failed payments
// TODO(username): Optimize query performance
// FIXME: Handle edge case when delivery time is null
Code Organization
File Structure
Each file should have a clear purpose:
// domain/orders/order.entity.ts
// 1. Imports
import { AggregateRoot } from '@/domain/shared/aggregate-root';
import Decimal from 'decimal.js';
// 2. Types/Interfaces
export interface OrderProps {
id: string;
// ...
}
// 3. Main class
export class Order extends AggregateRoot<OrderProps> {
// Public methods first
accept(): void {}
complete(): void {}
// Private methods last
private validateState(): void {}
}
// 4. Helper functions (if needed)
function calculateDeliveryDate(createdAt: Date, duration: number): Date {
// ...
}
Function Length
Keep functions focused and short:
// ✅ Correct - Small, focused function
function validateOrder(order: Order): void {
validateOrderStatus(order);
validatePayment(order);
validateDelivery(order);
}
function validateOrderStatus(order: Order): void {
if (order.status !== 'pending') {
throw new ValidationError('Order must be pending');
}
}
// ❌ Incorrect - Too long and complex
function validateOrder(order: Order): void {
// 50+ lines of validation logic
}
Best Practices Summary
✅ Do
- Use TypeScript strictly – Enable strict mode
- Follow layer boundaries – Respect Clean Architecture
- Write small functions – Single responsibility
- Use meaningful names – Self-documenting code
- Handle errors properly – Don't swallow exceptions
- Add types everywhere – Avoid
any - Use dependency injection – Pass dependencies
- Write tests – Test-driven development
- Document public APIs – JSDoc comments
- Run linter – Before committing
❌ Don't
- Don't use
any– Use proper types - Don't violate layers – Domain doesn't depend on Web
- Don't write god classes – Keep classes focused
- Don't ignore ESLint – Fix warnings
- Don't commit console.log – Use logger
- Don't hardcode values – Use constants
- Don't skip validation – Validate all inputs
- Don't forget error handling – Always handle errors
- Don't write long functions – Break them down
- Don't skip code review – Review all changes
Code Review Checklist
Before submitting code:
- [ ] Runs without errors –
pnpm devworks - [ ] Tests pass –
pnpm testsucceeds - [ ] Linter clean –
pnpm lintpasses - [ ] Formatted –
pnpm formatpasses - [ ] Type-safe – No TypeScript errors
- [ ] Documented – Public APIs have JSDoc
- [ ] Layer boundaries respected – Clean Architecture followed
- [ ] Error handling – All errors handled
- [ ] No console.log – Use logger instead
- [ ] Meaningful names – Clear variable/function names
Examples
Good Code Example
// domain/orders/order.entity.ts
import { AggregateRoot } from '@/domain/shared/aggregate-root';
import { InvalidStateError } from '@/domain/shared/exceptions';
import Decimal from 'decimal.js';
export interface OrderProps {
id: string;
status: OrderStatus;
price: Decimal;
createdAt: Date;
}
export type OrderStatus = 'pending' | 'accepted' | 'completed' | 'cancelled';
export class Order extends AggregateRoot<OrderProps> {
get status(): OrderStatus {
return this.props.status;
}
/**
* Accept the order and start processing.
* @throws {InvalidStateError} If order is not pending
*/
accept(): void {
if (this.props.status !== 'pending') {
throw new InvalidStateError(
'Order must be pending to accept'
);
}
this.props.status = 'accepted';
}
/**
* Mark order as completed.
* @throws {InvalidStateError} If order is not accepted
*/
complete(): void {
if (this.props.status !== 'accepted') {
throw new InvalidStateError(
'Order must be accepted to complete'
);
}
this.props.status = 'completed';
}
}
Bad Code Example
// ❌ Multiple violations
export class orderService { // Wrong: PascalCase for classes
repo: any; // Wrong: Don't use any
constructor(repo: any) { // Wrong: Don't use any
this.repo = repo
} // Wrong: Missing semicolons
async doStuff(id) { // Wrong: Missing types
let order = await this.repo.get(id) // Wrong: Missing semicolons
if (order.status == 'pending') { // Wrong: Use === not ==
console.log('Order is pending') // Wrong: Use logger
order.status = 'accepted' // Wrong: Direct mutation, use method
await this.repo.save(order)
return order
}
}
}
Summary
Maintaining consistent coding standards ensures:
- Readability – Easy to understand
- Maintainability – Easy to modify
- Scalability – Easy to extend
- Quality – Fewer bugs
- Collaboration – Team consistency
Follow these standards for professional, production-ready code.