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.