Skip to content

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:

// ✅ Correct
const message = "Hello, world!";

// ❌ Incorrect
const message = 'Hello, world!';

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:

# Check formatting
pnpm format

# Auto-fix formatting
pnpm prettier --write .

Formatting Rules

Indentation – 2 spaces:

function processOrder(order: Order) {
  if (order.isPaid) {
    return order.accept();
  }
  return null;
}

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:

user.entity.ts
service.repository.ts
order.usecases.ts
payment.validators.ts

Folders – lowercase:

domain/user/
domain/orders/
app/service/
adapters/account/

Code Naming

Classes – PascalCase:

class User extends AggregateRoot {}
class ServiceRepository extends BaseRepository {}
class CreateOrderDTO {}

Interfaces – PascalCase with descriptive names:

interface UserProps {}
interface OrderRepository {}
interface PaymentGateway {}

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:

Web → Application → Domain
Adapters → Application → Domain
// ✅ 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 errorspnpm dev works
  • [ ] Tests passpnpm test succeeds
  • [ ] Linter cleanpnpm lint passes
  • [ ] Formattedpnpm format passes
  • [ ] 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.