Skip to main content

Coding Conventions

This document establishes the coding standards and conventions for the CROW codebase. These rules ensure consistency, readability, and maintainability across all our code.

Philosophy

Our coding standards optimize for clarity over cleverness. Code should be self-explanatory, flat, and predictable. If you need to add a comment to explain what code does, the code needs to be rewritten.


TypeScript/JavaScript Standards

Base Style Guide

We follow the Google TypeScript Style Guide as our foundation, with specific overrides and additions documented below.

1. Comments Policy: No Comments (Self-Documenting Code)

Rule: Code must be self-documenting. Comments are considered a code smell.

Rationale: If code requires comments to be understood, it means the code itself is unclear. Instead of adding comments, refactor the code to be more readable.

What to do instead:

  • Use descriptive variable and function names
  • Extract complex logic into well-named functions
  • Break down large functions into smaller, focused ones
  • Use guard clauses and early returns

Exception: Public API documentation for exported interfaces is acceptable when necessary for external consumers.

// BAD: Using comments to explain unclear code
function calc(x: number, y: number): number {
// Calculate the discount based on order total
const d = x > 100 ? 0.2 : 0.1
// Apply discount and return
return y - (y * d)
}

// GOOD: Self-documenting code
function calculateDiscountedPrice(orderTotal: number, itemPrice: number): number {
const discountRate = orderTotal > 100 ? 0.2 : 0.1
const discountAmount = itemPrice * discountRate
return itemPrice - discountAmount
}

2. Maximum Indentation: 1 Level

Rule: Functions must not exceed 1 level of indentation (excluding the function body itself).

Rationale: Deep nesting creates cognitive load. Each indentation level multiplies the number of mental states developers must track. Flat code is easier to read, test, and maintain.

Techniques to achieve this:

  • Use guard clauses and early returns
  • Extract nested logic into separate functions
  • Use array methods (map, filter, reduce) instead of nested loops
  • Leverage polymorphism or lookup tables instead of nested conditionals
// BAD: Multiple levels of nesting
function processUser(user: User | null): string {
if (user) {
if (user.isActive) {
if (user.hasPermission('admin')) {
return 'Admin user processed'
} else {
return 'Regular user processed'
}
} else {
return 'Inactive user'
}
} else {
return 'No user found'
}
}

// GOOD: Guard clauses, single indentation level
function processUser(user: User | null): string {
if (!user) return 'No user found'
if (!user.isActive) return 'Inactive user'
if (user.hasPermission('admin')) return 'Admin user processed'

return 'Regular user processed'
}
// BAD: Nested loops
function findMatchingItems(orders: Order[]): Item[] {
const results: Item[] = []
for (const order of orders) {
for (const item of order.items) {
if (item.price > 100) {
results.push(item)
}
}
}
return results
}

// GOOD: Functional approach with single level
function findMatchingItems(orders: Order[]): Item[] {
return orders
.flatMap(order => order.items)
.filter(item => item.price > 100)
}

3. Paradigm: Functional Programming First

Rule: Default to functional programming patterns. Object-oriented approaches are a last resort, rarely needed.

Rationale: FP promotes immutability, predictability, and testability. Pure functions are easier to reason about, test, and debug than stateful objects.

Prefer:

  • Pure functions (same input → same output, no side effects)
  • Immutable data structures
  • Function composition
  • Data transformations over data mutation

Use OO only when:

  • Managing complex lifecycle or state (e.g., connection pools, streams)
  • Implementing required interfaces from external libraries
  • Creating boundary abstractions (DB clients, HTTP clients)
// BAD: Unnecessary OOP approach
class UserValidator {
private errors: string[] = []

validate(user: User): boolean {
this.errors = []
if (!user.email) this.errors.push('Email required')
if (!user.name) this.errors.push('Name required')
return this.errors.length === 0
}

getErrors(): string[] {
return this.errors
}
}

// GOOD: Functional approach
type ValidationResult = {
isValid: boolean
errors: string[]
}

function validateUser(user: User): ValidationResult {
const errors: string[] = []
if (!user.email) errors.push('Email required')
if (!user.name) errors.push('Name required')

return {
isValid: errors.length === 0,
errors
}
}

4. Composition Over Inheritance

Rule: Always prefer composition over inheritance. Inheritance is only acceptable for true "is-a" relationships.

Rationale: Inheritance creates tight coupling and fragile base class problems. Composition provides flexibility and clearer dependencies.

// BAD: Inheritance for code reuse
class Logger {
log(message: string): void {
console.log(message)
}
}

class UserService extends Logger {
createUser(name: string): void {
this.log(`Creating user: ${name}`)
}
}

// GOOD: Composition
type Logger = {
log: (message: string) => void
}

const createLogger = (): Logger => ({
log: (message) => console.log(message)
})

type UserService = {
createUser: (name: string) => void
}

const createUserService = (logger: Logger): UserService => ({
createUser: (name) => {
logger.log(`Creating user: ${name}`)
}
})

5. Architecture: Layer-Based Structure

Rule: Organize code by architectural layers (horizontal slicing).

Structure:

src/
├── controllers/ # HTTP handlers, route handlers
├── services/ # Business logic
├── repositories/ # Data access
├── models/ # Data types and schemas
├── utils/ # Shared utilities
└── config/ # Configuration

Rationale: Layer-based architecture makes the system structure immediately clear and enforces separation of concerns.

6. Error Handling: Exceptions at Boundaries

Rule: Throw exceptions only at integration boundaries. Add context when throwing.

Guidelines:

  • Throw exceptions at boundaries (HTTP, DB, external APIs)
  • Never swallow errors silently
  • Wrap exceptions with context
  • Log errors exactly once (at the boundary)
  • Let errors bubble up with meaningful context
// GOOD: Exception handling at boundaries
async function fetchUserFromDatabase(userId: string): Promise<User> {
try {
const result = await db.query('SELECT * FROM users WHERE id = ?', [userId])
return result.rows[0]
} catch (error) {
throw new Error(`Failed to fetch user ${userId}: ${error.message}`)
}
}

// GOOD: Letting errors bubble with context
async function getUserProfile(userId: string): Promise<UserProfile> {
const user = await fetchUserFromDatabase(userId)
const profile = transformUserToProfile(user)
return profile
}

7. Naming: Long Descriptive Names

Rule: Optimize for reading, not typing. Names should be long and descriptive.

Guidelines:

  • Use full words, avoid abbreviations
  • Boolean variables: prefix with is, has, should, can
  • Functions: use verb prefixes (get, fetch, create, update, delete, calculate, transform, validate)
  • Avoid vague names like data, info, manager, util, handler
// BAD: Unclear, abbreviated names
function proc(u: User): void {
const d = getData(u)
handle(d)
}

// GOOD: Clear, descriptive names
function processUserRegistration(user: User): void {
const userPreferences = getUserPreferences(user)
sendWelcomeEmail(userPreferences)
}

Naming conventions:

// Booleans
const isAuthenticated = true
const hasPermission = false
const shouldRedirect = true
const canEditProfile = false

// Functions
function getUserById(id: string): User
function createNewOrder(items: Item[]): Order
function calculateTotalPrice(items: Item[]): number
function transformUserToDTO(user: User): UserDTO
function validateEmailFormat(email: string): boolean

8. Function Size: Maximum 20-40 Lines

Rule: Functions should be small, typically 20-40 lines maximum.

Rationale: Small functions are easier to understand, test, and maintain. If a function is longer, it likely has multiple responsibilities.

How to achieve this:

  • Extract helper functions
  • Use array methods for collections
  • Apply the single responsibility principle
  • Break complex logic into named steps
// BAD: Long, multi-responsibility function
function processOrder(order: Order): void {
// Validate order (8 lines)
if (!order.items || order.items.length === 0) throw new Error('No items')
if (!order.customerId) throw new Error('No customer')
// ... more validation

// Calculate totals (10 lines)
let subtotal = 0
for (const item of order.items) {
subtotal += item.price * item.quantity
}
// ... more calculation

// Save to database (8 lines)
// ... database operations

// Send notifications (8 lines)
// ... email sending
}

// GOOD: Small, focused functions
function processOrder(order: Order): void {
validateOrder(order)
const orderTotal = calculateOrderTotal(order)
saveOrderToDatabase(order, orderTotal)
sendOrderConfirmationEmail(order)
}

function validateOrder(order: Order): void {
if (!order.items || order.items.length === 0) throw new Error('No items')
if (!order.customerId) throw new Error('No customer')
}

function calculateOrderTotal(order: Order): number {
return order.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
}

9. Testing: Coverage Thresholds Required

Rule: Maintain minimum code coverage thresholds. PRs must maintain or improve coverage.

Requirements:

  • Business logic functions must have tests
  • Aim for meaningful coverage, not just hitting numbers
  • Focus on testing behavior, not implementation details
  • Test edge cases and error conditions

What to test:

  • Business logic and calculations
  • Data transformations
  • Validation functions
  • Error handling paths

What not to test:

  • Simple getters/setters
  • Framework glue code
  • Third-party library wrappers (unless adding logic)

Commit Messages

We use commitlint for consistent commit formatting.

Format: <type>: <subject>

Types: feat, fix, docs, style, refactor, test, chore

Examples:

feat: add authentication
fix: resolve timeout issue
docs: update readme
refactor: extract user validation logic
test: add coverage for order processing

Enforcement

These standards are enforced through:

  • Code reviews (all PRs require approval)
  • ESLint configuration based on @antfu/eslint-config
  • Prettier for formatting
  • Pre-commit hooks via Husky
  • CI pipeline checks

Remember: These rules exist to make our codebase more maintainable and easier to work with. When in doubt, optimize for the person reading your code six months from now.