7.1 KiB
Code Generation Patterns
Purpose
Standards and patterns for generating clean, maintainable code. These principles apply regardless of language and ensure generated code is production-worthy, not prototype-quality.
Clean Code Principles
Naming
- Variables: Describe what it holds, not the type. Use
customerEmailnotstr1ordata. - Functions: Describe what it does using a verb. Use
calculateShippingCost()notprocess()ordoWork(). - Booleans: Use
is,has,can,shouldprefixes. UseisActivenotactiveorflag. - Constants: Use SCREAMING_SNAKE_CASE for true constants. Use
MAX_RETRY_COUNTnotnum. - Classes: Use nouns. Use
OrderProcessornotProcessOrdersorOrderHelper.
Functions
- Do one thing. If the function name includes "and", split it into two functions.
- Keep parameter count low (0-3 ideal; more than 3 suggests a parameter object is needed).
- Avoid boolean parameters that switch behavior — use two clearly named functions instead.
- Return early for guard clauses to reduce nesting depth.
Files
- One primary concept per file (one class, one module, one component).
- Keep files under 300 lines. If longer, the concept is likely doing too much.
- Group related files by feature, not by type (see "Code Organization" below).
SOLID Principles Applied
Single Responsibility (S)
Each module/class has one reason to change.
// Bad: UserService handles auth, profile, and notifications
// Good: AuthService, ProfileService, NotificationService
Open/Closed (O)
Extend behavior without modifying existing code. Use interfaces and composition.
// Bad: Adding a new payment type requires modifying PaymentProcessor
// Good: PaymentProcessor accepts a PaymentStrategy interface; add new strategies without touching the processor
Liskov Substitution (L)
Subtypes must be substitutable for their base types without breaking behavior. If overriding a method changes the contract, the inheritance hierarchy is wrong.
Interface Segregation (I)
No client should depend on methods it does not use. Prefer small, focused interfaces over large ones.
// Bad: interface Repository { find, save, delete, export, import, backup }
// Good: interface Readable { find }, interface Writable { save, delete }
Dependency Inversion (D)
High-level modules depend on abstractions, not concrete implementations. Pass dependencies in; do not construct them internally.
// Bad: class OrderService { db = new PostgresDB() }
// Good: class OrderService { constructor(db: Database) }
Error Handling Patterns
Result Type (Preferred)
Return success/failure explicitly instead of throwing exceptions for expected failures.
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function parseEmail(input: string): Result<Email, ValidationError> {
if (!isValidEmail(input)) {
return { ok: false, error: new ValidationError('Invalid email format') };
}
return { ok: true, value: new Email(input) };
}
Try-Catch Hierarchy
When using exceptions, follow this hierarchy:
- Catch specific exceptions first — handle known, recoverable errors
- Let unknown exceptions propagate — do not catch
ExceptionorErrorbroadly - Catch at boundaries — API handlers, event processors, and CLI entry points are appropriate catch-all locations
- Never swallow exceptions silently —
catch (e) {}hides bugs
Error Messages
- Include what happened, why it happened, and what the user/developer can do about it
- Include relevant context (IDs, input values, operation being performed)
- Do not expose internal implementation details in user-facing errors
Logging Standards
Log Levels
- ERROR: Something failed that requires attention. Include enough context to diagnose.
- WARN: Something unexpected happened but was handled. May indicate a developing issue.
- INFO: Significant business events (order placed, user registered, payment processed). One per operation.
- DEBUG: Detailed diagnostic information for development. Never log sensitive data at any level.
Structured Logging
{
"level": "INFO",
"message": "Order placed successfully",
"orderId": "ord-123",
"customerId": "cust-456",
"totalAmount": 99.99,
"timestamp": "2024-01-15T10:30:00Z",
"correlationId": "req-789"
}
Rules
- Log at operation boundaries (start, success, failure) — not inside loops
- Include correlation/request IDs for tracing across services
- Never log: passwords, tokens, API keys, PII, credit card numbers
- Use structured format (JSON) not free-text strings for production logs
Input Validation at Boundaries
Boundary Definition
Validate input at every trust boundary — where data enters your system from an untrusted source:
- API request handlers (HTTP, gRPC, GraphQL)
- Event/message consumers (SQS, Kafka, EventBridge)
- File upload processors
- CLI argument parsers
- Database query results from external systems
Validation Strategy
External Input → Validate at boundary → Convert to domain type → Domain logic uses typed values
What to Validate
- Presence: Required fields exist and are not null/empty
- Type: Values are the expected type (string, number, date)
- Range: Numbers within acceptable bounds, strings within length limits
- Format: Emails, URLs, dates, phone numbers match expected patterns
- Business rules: Values are valid in context (status transitions, referential integrity)
After Validation
Once data passes the boundary and is converted to a domain type, internal code should NOT re-validate. Trust the boundary. This keeps domain logic clean and focused on business rules.
Code Organization
By Feature (Recommended)
/src
/orders
order.ts # domain model
order-service.ts # business logic
order-repository.ts # data access
order-handler.ts # API handler
order.test.ts # tests
/customers
customer.ts
customer-service.ts
...
By Layer (Avoid for Medium-Large Projects)
/src
/models # all domain models from all features
/services # all services from all features
/repositories # all repositories from all features
/handlers # all handlers from all features
Why Feature-Based is Better
- Related code is co-located — changes to "orders" touch files in one directory
- Easy to understand the scope of a feature by looking at one directory
- Supports extraction to microservices later (each feature directory is a candidate)
- Layer-based organization scatters related code across the entire project
Code Generation Checklist
- Follows naming conventions (descriptive, consistent, no abbreviations)
- Functions are small and single-purpose
- Error handling is explicit (Result type or catch at boundaries)
- Input validation at all trust boundaries
- Structured logging at operation boundaries
- No hardcoded secrets, URLs, or configuration values
- Dependencies are injected, not constructed internally
- Tests accompany all generated code
- Code is organized by feature, not by layer