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 readable, flat, and predictable. Every function must include a brief comment describing what it does so that developers unfamiliar with the codebase can quickly understand its purpose.


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: One-Line Function Summary Required

Rule: Every function must have a single-line comment immediately above it that describes what the function does.

Rationale: Because not every developer is familiar with the entire codebase, a concise description above each function makes it immediately clear what that function is responsible for — without needing to read its entire body.

Guidelines:

  • Keep the comment short (one sentence is usually enough)
  • Describe what the function does, not how it does it
  • Write the comment in plain English, starting with a verb (e.g., "Calculates…", "Returns…", "Validates…", "Sends…")
  • Do not repeat the function name verbatim; add meaningful context

What to avoid:

  • Inline comments explaining individual lines of code (refactor instead)
  • Vague comments like // does stuff or // helper
  • Comments that are longer than necessary
// BAD: No comment, and unclear code
function calc(x: number, y: number): number {
const d = x > 100 ? 0.2 : 0.1
return y - (y * d)
}

// BAD: Inline comments instead of a function summary
function calculateDiscountedPrice(orderTotal: number, itemPrice: number): number {
// Use 20% discount for orders over 100, otherwise 10%
const discountRate = orderTotal > 100 ? 0.2 : 0.1
// Deduct discount from item price
const discountAmount = itemPrice * discountRate
return itemPrice - discountAmount
}

// GOOD: Single summary comment above the function
// Calculates the final price of an item after applying an order-size discount.
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, with function summary comment
// Returns a status string describing how the given user was processed, or why it was not.
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 and function summary comment
// Returns all items across the given orders whose price exceeds 100.
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 with function summary comment
type ValidationResult = {
isValid: boolean
errors: string[]
}

// Validates the required fields of a user object and returns the result with any error messages.
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 with function summary comments
type Logger = {
log: (message: string) => void
}

// Creates and returns a logger that writes messages to the console.
const createLogger = (): Logger => ({
log: (message) => console.log(message)
})

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

// Creates a user service that uses the provided logger to record user creation events.
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
// Fetches a user record from the database by ID, throwing a descriptive error on failure.
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
// Retrieves and returns the full profile for the given user ID.
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 with function summary comment
// Fetches the user's preferences and sends them a welcome email.
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, each with a summary comment
// Validates the order, calculates the total, persists it, and sends a confirmation email.
function processOrder(order: Order): void {
validateOrder(order)
const orderTotal = calculateOrderTotal(order)
saveOrderToDatabase(order, orderTotal)
sendOrderConfirmationEmail(order)
}

// Throws an error if the order is missing required fields.
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')
}

// Returns the sum of all item prices multiplied by their quantities.
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.