Clean Code — Rules and Conventions
1. Philosophy
- Code is read more than written — optimize for readability.
- Explicit over implicit — clear intent beats clever shortcuts.
- Consistency over preference — follow project conventions, not personal style.
- Refactor mercilessly — technical debt compounds; pay it down continuously.
- Tests are documentation — they specify behavior better than comments.
2. Naming
Principles
| Principle |
Example |
| Intention-revealing |
calculateShippingCost not calc() |
| Pronounceable |
userCount not usrCnt |
| Searchable |
MAX_RETRY_ATTEMPTS not 3 |
| No mental mapping |
isActive not flag |
| Domain vocabulary |
Order.place() not createOrder() |
Conventions
| Type |
Convention |
Example |
| Variables/Functions |
camelCase |
getUserById, maxRetries |
| Classes/Interfaces |
PascalCase |
UserService, OrderRepository |
| Constants |
SCREAMING_SNAKE |
MAX_RETRY_ATTEMPTS |
| Types/Interfaces |
PascalCase |
User, OrderDto |
| Type parameters |
PascalCase |
<TUser, TOrder> |
| Private fields |
_camelCase |
_cache, _config |
| Boolean |
is/has/can/should |
isActive, hasPermission |
Rules
- No abbreviations —
userId not uid, calculateTotal not calcTot
- No type in name —
users not userList, getUser not getUserObject
- One word per concept —
fetch, retrieve, get — pick one
- No magic numbers —
const MAX_RETRIES = 3
3. Functions
Principles Functions
- Small — 10-20 lines max
- Single responsibility — does one thing well
- Pure when possible — same input → same output, no side effects
- Few parameters — 0-2 ideal, 3 max (use object for more)
Patterns
// ✅ Good: small, focused, descriptive
function calculateShippingCost(order: Order, address: Address): Money {
const baseRate = getBaseRate(address.zone);
const weightSurcharge = calculateWeightSurcharge(order.totalWeight);
return baseRate.add(weightSurcharge);
}
// ❌ Bad: large, multiple responsibilities
function processOrder(order, address, user, sendEmail, saveToDb) {
// 50 lines of validation, calculation, email, db save...
}
Rules Functions
- Extract until you can't — if function does A then B, split
- Guard clauses first — early return for edge cases
- No output parameters — return values, don't mutate args
- No flag arguments —
renderUser(user, true) → renderAdminUser(user)
4. Early Return / Guard Clauses
// ✅ Good: flat, readable
function processPayment(payment: Payment): Result {
if (!payment.isValid()) return err(new InvalidPaymentError());
if (payment.amount.lte(0)) return err(new InvalidAmountError());
if (payment.expired()) return err(new ExpiredPaymentError());
return process(payment);
}
// ❌ Bad: nested, hard to follow
function processPayment(payment: Payment): Result {
if (payment.isValid()) {
if (payment.amount.gt(0)) {
if (!payment.expired()) {
return process(payment);
}
}
}
return err(new PaymentError());
}
Rules Early Return
- Return early — reduce nesting
- Validate inputs first — fail fast
- One exit point not required — early returns improve readability
5. Error Handling
Custom Errors
// errors/domain.ts
export class DomainError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly context?: Record<string, unknown>,
) {
super(message);
this.name = this.constructor.name;
}
}
export class OrderNotFoundError extends DomainError {
constructor(id: OrderId) {
super(`Order ${id} not found`, "ORDER_NOT_FOUND", { orderId: id.value });
}
}
export class InsufficientInventoryError extends DomainError {
constructor(productId: ProductId, requested: number, available: number) {
super(`Insufficient inventory for ${productId}`, "INSUFFICIENT_INVENTORY", {
productId: productId.value,
requested,
available,
});
}
}
Result Pattern
// types/result.ts
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Usage
async function placeOrder(
cmd: PlaceOrderCommand,
): Promise<Result<OrderId, DomainError>> {
const customer = await customerRepo.findById(cmd.customerId);
if (!customer) return err(new CustomerNotFoundError(cmd.customerId));
const order = Order.place(customer, cmd.items);
await orderRepo.save(order);
return ok(order.id);
}
Rules Error Handling
- No throwing for control flow — use Result pattern
- Errors are values — pass them, don't throw
- Context in errors — include relevant IDs, values
- Typed errors — discriminant union for exhaustive handling
6. Comments
When to Comment
// ✅ Why (not what)
function calculateTax(amount: Money): Money {
// Jurisdiction X requires rounding UP to nearest cent
return amount.multiply(TAX_RATE).roundUp();
}
// ❌ What (code already says this)
function add(a: number, b: number): number {
// Adds a and b
return a + b;
}
// ✅ Warn about non-obvious behavior
function getUser(id: UserId): Promise<User> {
// Cache-first: checks Redis before DB. Stale data possible for 5min.
return cache.getOrFetch(`user:${id}`, () => db.findUser(id));
}
Rules Comments
- Code says what, comments say why
- No commented-out code — delete it, git has history
- No TODOs without issue link —
// TODO(#123): refactor
- JSDoc for public APIs — params, returns, throws
7. Testing (AAA Pattern)
// tests/order.test.ts
import { describe, it, expect } from "vitest";
describe("Order", () => {
describe("place", () => {
it("creates order with pending status", () => {
// Arrange
const customer = new Customer(new CustomerId("cust-1"), "John");
const items = [new OrderItem(new ProductId("prod-1"), 2)];
// Act
const order = Order.place(customer.id, items);
// Assert
expect(order.status).toBe("pending");
expect(order.items).toHaveLength(1);
expect(order.customerId).toEqual(customer.id);
});
it("throws when items empty", () => {
// Arrange
const customer = new Customer(new CustomerId("cust-1"), "John");
// Act & Assert
expect(() => Order.place(customer.id, [])).toThrow(
"Order must have items",
);
});
});
});
Rules Testing
- AAA — Arrange, Act, Assert
- One assertion per test — or related group
- Descriptive names —
creates_order_with_pending_status
- Test behavior, not implementation — public API only
- Fast, isolated, deterministic — no DB, no network
8. Code Organization
File Structure
src/
├── domain/ # Pure business logic
│ ├── order/
│ │ ├── Order.ts
│ │ ├── OrderItem.ts
│ │ └── OrderRepository.ts (interface)
│ └── shared/
│ ├── Email.ts
│ └── Money.ts
├── application/ # Use cases / orchestration
│ └── orders/
│ ├── PlaceOrderCommand.ts
│ ├── PlaceOrderHandler.ts
│ └── GetOrderQuery.ts
├── infrastructure/ # External concerns
│ ├── repositories/
│ │ └── PrismaOrderRepository.ts
│ └── email/
│ └── SendGridEmailService.ts
└── presentation/ # HTTP, CLI, GraphQL
└── orders/
└── OrdersController.ts
Rules Code Organization
- Layer separation — domain knows nothing of infrastructure
- Dependency inversion — domain defines interfaces, infra implements
- Colocate related — tests next to code, types next to usage
9. SOLID (Compact)
| Principle |
Rule |
| Single Responsibility |
One reason to change |
| Open/Closed |
Open for extension, closed for modification |
| Liskov Substitution |
Subtypes substitutable for base |
| Interface Segregation |
Many specific > one general |
| Dependency Inversion |
Depend on abstractions, not concretions |
Practical Application
// ❌ Violates DIP
class OrderService {
private db = new Database(); // Concrete dependency
}
// ✅ Follows DIP
class OrderService {
constructor(private repo: OrderRepository) {} // Interface
}
10. Methodology
Before using ANY clean code pattern not documented in
this skill:
- MCP Context7 (priority):
context7_resolve-library-id +
context7_query-docs for clean code, refactoring patterns.
- Official docs: Clean Code (Martin), Refactoring (Fowler)
— verify current patterns.
- Project config:
tsconfig.json, eslint.config.js,
package.json — verify against actual setup.
- HARD RULE: If not in this skill AND cannot be verified against
2 authoritative sources → DO NOT USE IT. Document as assumption or risk in
report to orchestrator.
10. Prohibitions
- ❌ No
any — use unknown + narrowing
- ❌ No magic numbers — use named constants
- ❌ No
console.log in production code
- ❌ No
// TODO without issue reference
- ❌ No dead code — delete, don't comment
- ❌ No
try/catch for control flow — use Result
- ❌ No
any in public APIs
- ❌ No commented-out code — git has history
11. References
Note: For TypeScript rules, see TypeScript
Note: For JavaScript conventions, see JavaScript
Note: For Error handling, see Security
Note: For Testing patterns, see Testing
Last updated: 2026-08
1---2name: clean-code3description: Clean code rules - KISS, DRY, YAGNI, SOLID, descriptive names, small functions, early return, custom errors, effective comments, readable tests (AAA)4---56# Clean Code — Rules and Conventions78---910## 1. Philosophy11121. **Code is read more than written** — optimize for readability.132. **Explicit over implicit** — clear intent beats clever shortcuts.143. **Consistency over preference** — follow project conventions, not personal style.154. **Refactor mercilessly** — technical debt compounds; pay it down continuously.165. **Tests are documentation** — they specify behavior better than comments.1718---1920## 2. Naming2122### Principles2324| Principle | Example |25| ----------------------- | ------------------------------------ |26| **Intention-revealing** | `calculateShippingCost` not `calc()` |27| **Pronounceable** | `userCount` not `usrCnt` |28| **Searchable** | `MAX_RETRY_ATTEMPTS` not `3` |29| **No mental mapping** | `isActive` not `flag` |30| **Domain vocabulary** | `Order.place()` not `createOrder()` |3132### Conventions3334| Type | Convention | Example |35| ----------------------- | ----------------- | -------------------------------- |36| **Variables/Functions** | camelCase | `getUserById`, `maxRetries` |37| **Classes/Interfaces** | PascalCase | `UserService`, `OrderRepository` |38| **Constants** | SCREAMING_SNAKE | `MAX_RETRY_ATTEMPTS` |39| **Types/Interfaces** | PascalCase | `User`, `OrderDto` |40| **Type parameters** | PascalCase | `<TUser, TOrder>` |41| **Private fields** | \_camelCase | `_cache`, `_config` |42| **Boolean** | is/has/can/should | `isActive`, `hasPermission` |4344### Rules4546- **No abbreviations** — `userId` not `uid`, `calculateTotal` not `calcTot`47- **No type in name** — `users` not `userList`, `getUser` not `getUserObject`48- **One word per concept** — `fetch`, `retrieve`, `get` — pick one49- **No magic numbers** — `const MAX_RETRIES = 3`5051---5253## 3. Functions5455### Principles Functions5657- **Small** — 10-20 lines max58- **Single responsibility** — does one thing well59- **Pure when possible** — same input → same output, no side effects60- **Few parameters** — 0-2 ideal, 3 max (use object for more)6162### Patterns6364```ts65// ✅ Good: small, focused, descriptive66function calculateShippingCost(order: Order, address: Address): Money {67 const baseRate = getBaseRate(address.zone);68 const weightSurcharge = calculateWeightSurcharge(order.totalWeight);69 return baseRate.add(weightSurcharge);70}7172// ❌ Bad: large, multiple responsibilities73function processOrder(order, address, user, sendEmail, saveToDb) {74 // 50 lines of validation, calculation, email, db save...75}76```7778### Rules Functions7980- **Extract until you can't** — if function does A then B, split81- **Guard clauses first** — early return for edge cases82- **No output parameters** — return values, don't mutate args83- **No flag arguments** — `renderUser(user, true)` → `renderAdminUser(user)`8485---8687## 4. Early Return / Guard Clauses8889```ts90// ✅ Good: flat, readable91function processPayment(payment: Payment): Result {92 if (!payment.isValid()) return err(new InvalidPaymentError());93 if (payment.amount.lte(0)) return err(new InvalidAmountError());94 if (payment.expired()) return err(new ExpiredPaymentError());9596 return process(payment);97}9899// ❌ Bad: nested, hard to follow100function processPayment(payment: Payment): Result {101 if (payment.isValid()) {102 if (payment.amount.gt(0)) {103 if (!payment.expired()) {104 return process(payment);105 }106 }107 }108 return err(new PaymentError());109}110```111112### Rules Early Return113114- **Return early** — reduce nesting115- **Validate inputs first** — fail fast116- **One exit point not required** — early returns improve readability117118---119120## 5. Error Handling121122### Custom Errors123124```ts125// errors/domain.ts126export class DomainError extends Error {127 constructor(128 message: string,129 public readonly code: string,130 public readonly context?: Record<string, unknown>,131 ) {132 super(message);133 this.name = this.constructor.name;134 }135}136137export class OrderNotFoundError extends DomainError {138 constructor(id: OrderId) {139 super(`Order ${id} not found`, "ORDER_NOT_FOUND", { orderId: id.value });140 }141}142143export class InsufficientInventoryError extends DomainError {144 constructor(productId: ProductId, requested: number, available: number) {145 super(`Insufficient inventory for ${productId}`, "INSUFFICIENT_INVENTORY", {146 productId: productId.value,147 requested,148 available,149 });150 }151}152```153154### Result Pattern155156```ts157// types/result.ts158type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };159160function ok<T>(value: T): Result<T, never> {161 return { ok: true, value };162}163function err<E>(error: E): Result<never, E> {164 return { ok: false, error };165}166167// Usage168async function placeOrder(169 cmd: PlaceOrderCommand,170): Promise<Result<OrderId, DomainError>> {171 const customer = await customerRepo.findById(cmd.customerId);172 if (!customer) return err(new CustomerNotFoundError(cmd.customerId));173174 const order = Order.place(customer, cmd.items);175 await orderRepo.save(order);176 return ok(order.id);177}178```179180### Rules Error Handling181182- **No throwing for control flow** — use Result pattern183- **Errors are values** — pass them, don't throw184- **Context in errors** — include relevant IDs, values185- **Typed errors** — discriminant union for exhaustive handling186187---188189## 6. Comments190191### When to Comment192193```ts194// ✅ Why (not what)195function calculateTax(amount: Money): Money {196 // Jurisdiction X requires rounding UP to nearest cent197 return amount.multiply(TAX_RATE).roundUp();198}199200// ❌ What (code already says this)201function add(a: number, b: number): number {202 // Adds a and b203 return a + b;204}205206// ✅ Warn about non-obvious behavior207function getUser(id: UserId): Promise<User> {208 // Cache-first: checks Redis before DB. Stale data possible for 5min.209 return cache.getOrFetch(`user:${id}`, () => db.findUser(id));210}211```212213### Rules Comments214215- **Code says what, comments say why**216- **No commented-out code** — delete it, git has history217- **No TODOs without issue link** — `// TODO(#123): refactor`218- **JSDoc for public APIs** — params, returns, throws219220---221222## 7. Testing (AAA Pattern)223224```ts225// tests/order.test.ts226import { describe, it, expect } from "vitest";227228describe("Order", () => {229 describe("place", () => {230 it("creates order with pending status", () => {231 // Arrange232 const customer = new Customer(new CustomerId("cust-1"), "John");233 const items = [new OrderItem(new ProductId("prod-1"), 2)];234235 // Act236 const order = Order.place(customer.id, items);237238 // Assert239 expect(order.status).toBe("pending");240 expect(order.items).toHaveLength(1);241 expect(order.customerId).toEqual(customer.id);242 });243244 it("throws when items empty", () => {245 // Arrange246 const customer = new Customer(new CustomerId("cust-1"), "John");247248 // Act & Assert249 expect(() => Order.place(customer.id, [])).toThrow(250 "Order must have items",251 );252 });253 });254});255```256257### Rules Testing258259- **AAA** — Arrange, Act, Assert260- **One assertion per test** — or related group261- **Descriptive names** — `creates_order_with_pending_status`262- **Test behavior, not implementation** — public API only263- **Fast, isolated, deterministic** — no DB, no network264265---266267## 8. Code Organization268269### File Structure270271```text272src/273├── domain/ # Pure business logic274│ ├── order/275│ │ ├── Order.ts276│ │ ├── OrderItem.ts277│ │ └── OrderRepository.ts (interface)278│ └── shared/279│ ├── Email.ts280│ └── Money.ts281├── application/ # Use cases / orchestration282│ └── orders/283│ ├── PlaceOrderCommand.ts284│ ├── PlaceOrderHandler.ts285│ └── GetOrderQuery.ts286├── infrastructure/ # External concerns287│ ├── repositories/288│ │ └── PrismaOrderRepository.ts289│ └── email/290│ └── SendGridEmailService.ts291└── presentation/ # HTTP, CLI, GraphQL292 └── orders/293 └── OrdersController.ts294```295296### Rules Code Organization297298- **Layer separation** — domain knows nothing of infrastructure299- **Dependency inversion** — domain defines interfaces, infra implements300- **Colocate related** — tests next to code, types next to usage301302---303304## 9. SOLID (Compact)305306| Principle | Rule |307| ------------------------- | ------------------------------------------- |308| **S**ingle Responsibility | One reason to change |309| **O**pen/Closed | Open for extension, closed for modification |310| **L**iskov Substitution | Subtypes substitutable for base |311| **I**nterface Segregation | Many specific > one general |312| **D**ependency Inversion | Depend on abstractions, not concretions |313314### Practical Application315316```ts317// ❌ Violates DIP318class OrderService {319 private db = new Database(); // Concrete dependency320}321322// ✅ Follows DIP323class OrderService {324 constructor(private repo: OrderRepository) {} // Interface325}326```327328---329330## 10. Methodology331332Before using ANY clean code pattern not documented in333this skill:3343351. **MCP Context7** (priority): `context7_resolve-library-id` +336 `context7_query-docs` for clean code, refactoring patterns.3372. **Official docs**: Clean Code (Martin), Refactoring (Fowler)338 — verify current patterns.3393. **Project config**: `tsconfig.json`, `eslint.config.js`,340 `package.json` — verify against actual setup.3414. **HARD RULE**: If not in this skill AND cannot be verified against342 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in343 report to orchestrator.344345---346347## 10. Prohibitions348349- ❌ No `any` — use `unknown` + narrowing350- ❌ No magic numbers — use named constants351- ❌ No `console.log` in production code352- ❌ No `// TODO` without issue reference353- ❌ No dead code — delete, don't comment354- ❌ No `try/catch` for control flow — use Result355- ❌ No `any` in public APIs356- ❌ No commented-out code — git has history357358---359360## 11. References361362> **Note:** For TypeScript rules, see [TypeScript](../typescript/SKILL.md)363> **Note:** For JavaScript conventions, see [JavaScript](../javascript/SKILL.md)364> **Note:** For Error handling, see [Security](../security/SKILL.md)365> **Note:** For Testing patterns, see [Testing](../testing/SKILL.md)366367---368369Last updated: 2026-08