Typescript rules backend
Skill tundraray/overture/backend/skills/typescript-rules-backend
This skill provides backend TypeScript development rules including functions-over-classes design, NestJS/TypeORM patterns, layered error handling, streaming, and memory management. Automatically loaded when implementing backend TypeScript services, APIs, or when "NestJS", "backend TypeScript", "API service", "TypeORM", or "server-side TypeScript" are mentioned.From its SKILL.md
npx -y skills add tundraray/overture --skill typescript-rules-backendAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
11.0 KB, as published. Nobody here has run it
TypeScript Development Rules (Backend)
Basic Principles
- Functions Over Classes: Prefer pure functions and function composition. Use classes only when the framework requires it (NestJS controllers/services, TypeORM entities)
- YAGNI Principle: Don't implement until necessary — no speculative abstractions
- Aggressive Refactoring: Prevent technical debt; delete unused code immediately
Comment Writing Rules
- Function Description Focus: Describe what the code "does"
- No Historical Information: Do not record development history
- Timeless: Write only content that remains valid whenever read
- Conciseness: Keep explanations to necessary minimum
Type Safety
Absolute Rule: any type is completely prohibited. It disables type checking and becomes a source of runtime errors.
any Type Alternatives (Priority Order)
unknownType + Type Guards: Use for validating external input (API request bodies, environment variables, message queue payloads)- Generics: When type flexibility is needed across services
- Union Types / Intersection Types: Combinations of multiple types
- Type Assertions (Last Resort): Only when type is certain and documented
Type Guard Implementation Pattern
function isCreateUserDto(value: unknown): value is CreateUserDto {
return typeof value === 'object' && value !== null && 'email' in value && 'name' in value
}
Modern Type Features
satisfiesOperator:const config = { port: 3000 } satisfies AppConfig— Preserves inferenceconstAssertion:const ROLES = ['admin', 'user', 'viewer'] as const— Immutable and type-safe- Branded Types:
type UserId = string & { __brand: 'UserId' }— Distinguish domain identifiers - Template Literal Types:
type EventName = \on${Capitalize<string>}`` — Express string patterns with types
Type Safety in Backend Implementation
- API Request Bodies: Always receive as
unknown, validate with Zod/class-validator before processing - Environment Variables: Treat as
unknown, validate at startup — fail fast on missing required values - Database Query Results: Trust ORM type mappings; validate raw query results
- Message Queue Payloads: Treat as
unknown, validate with schema before processing - Configuration Files: Validate at application startup, not at usage sites
Type Safety in Data Flow
- Client → Server: Request body (
unknown) → Validation (Zod/class-validator) → DTO (Type Guaranteed) → Service - Server → Database: DTO → Entity mapping → ORM (Type Guaranteed) → Database
- Database → Server: Query result (ORM typed) → Response DTO → Serialization → Client
Type Complexity Management
- DTO Design: Keep DTOs flat; max 2 levels of nesting. Split complex DTOs into composition
- Generic Services: Max 3 type parameters. If more needed, reconsider design
- Type Assertions: Review design if used 3+ times in a module
Coding Conventions
Function Design
- Functions Over Classes: Default to exported functions for business logic
- 0-2 parameters maximum: Use object for 3+ parameters
// ✅ Object parameter function createUser({ name, email, role }: CreateUserParams): Promise<User> {} - Pure Functions: Minimize side effects; isolate I/O at boundaries
- Single Responsibility: One function does one thing
NestJS Patterns (when using NestJS)
- Controllers: Thin — validate input, call service, return response. No business logic
- Services: Business logic lives here. Inject repositories, not raw database connections
- Repositories: Data access layer. Custom repository methods for complex queries
- Modules: Feature-based module organization. Avoid circular dependencies
- Guards/Interceptors/Pipes: Cross-cutting concerns — auth, logging, validation, transformation
// ✅ Thin controller — delegates to service
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
return this.userService.create(dto)
}
}
TypeORM Patterns (when using TypeORM)
- Entities: Define with decorators. Use strict column types matching database schema
- Migrations: Always use migrations for schema changes. Never use
synchronize: truein production - Query Builder: Prefer repository methods. Use QueryBuilder only for complex joins/aggregations
- Relations: Define as lazy or eager explicitly. Avoid N+1 queries
Dependency Injection
- Constructor Injection: Standard pattern for NestJS services
- Factory Functions: For non-NestJS modules, use factory functions that accept dependencies as parameters
Asynchronous Processing
- Promise Handling: Always use
async/await - Concurrent Operations: Use
Promise.all()for independent operations,Promise.allSettled()when partial failure is acceptable - Stream Processing: Use Node.js streams for large data sets (files, DB result sets)
Environment Variables
- Validate at startup: Fail fast if required variables are missing
- Centralize configuration: Single config module/service that validates and exports typed config
- Never access
process.envdirectly in business logic — always go through config layer
// ✅ Centralized, validated config
const config = {
port: parseInt(process.env.PORT || '3000', 10),
dbUrl: requiredEnv('DATABASE_URL'),
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
} satisfies AppConfig
function requiredEnv(key: string): string {
const value = process.env[key]
if (!value) throw new Error(`Missing required environment variable: ${key}`)
return value
}
Security
- Input Validation: Validate all external input at API boundary (controllers/middleware)
- SQL Injection: Use parameterized queries — never concatenate user input into queries
- Secret Management: Never log secrets. Use environment variables, not hardcoded values
- Rate Limiting: Apply to public endpoints. Use guards/middleware, not service logic
- Authentication/Authorization: Implement as guards, not inline checks in controllers
Format Rules
- Semicolon omission (follow project linter settings)
- Types in
PascalCase, variables/functions incamelCase - Use path aliases for imports (
@/,@app/, etc.)
Clean Code Principles
- Delete unused code immediately
- Delete debug
console.log() - No commented-out code (manage history with version control)
- Comments explain "why" (not "what")
Error Handling
Absolute Rule: Error suppression prohibited. All errors must have log output and appropriate handling.
Layered Error Handling (Backend)
Three distinct layers, each with specific responsibilities:
-
API Layer (Controllers/Middleware)
- Catch service errors, map to HTTP status codes
- Return consistent error response format
- Log request context (method, path, user ID)
-
Service Layer (Business Logic)
- Throw domain-specific errors (ValidationError, NotFoundError, ConflictError)
- Include context in error messages (what was being done, with what data)
- Never catch and suppress — either handle or propagate
-
Repository Layer (Data Access)
- Convert database errors to domain errors
- Handle connection failures, deadlocks, constraint violations
- Retry transient failures (connection drops, deadlocks) with backoff
// ✅ Layered error handling
// Repository layer
async findById(id: string): Promise<User> {
const user = await this.repository.findOne({ where: { id } })
if (!user) throw new NotFoundError(`User not found: ${id}`)
return user
}
// Service layer
async updateUser(id: string, dto: UpdateUserDto): Promise<User> {
const user = await this.userRepository.findById(id) // Throws NotFoundError
Object.assign(user, dto)
return this.userRepository.save(user)
}
// Controller layer (NestJS exception filter handles mapping)
@Put(':id')
async update(@Param('id') id: string, @Body() dto: UpdateUserDto): Promise<UserResponseDto> {
return this.userService.updateUser(id, dto)
}
Fail-Fast Principle: Fail quickly on errors to prevent continued processing in invalid states
// ✅ Required: Explicit failure
catch (error) {
logger.error('Processing failed', { error, context })
throw error
}
Custom Error Classes
export class AppError extends Error {
constructor(message: string, public readonly code: string, public readonly statusCode = 500) {
super(message)
this.name = this.constructor.name
}
}
// Purpose-specific: ValidationError(400), NotFoundError(404), ConflictError(409), ApiError(502)
Structured Logging
- Use structured logger (pino, winston) — not
console.log - Include correlation IDs for request tracing
- Never log sensitive information (passwords, tokens, API keys, PII)
Streaming and Memory Management
Stream Processing
- Use Node.js streams for processing files, large database result sets, and bulk operations
- Prefer
pipeline()over.pipe()for proper error handling and cleanup
import { pipeline } from 'stream/promises'
// ✅ Stream large file processing
await pipeline(
createReadStream('input.csv'),
new CsvParser(),
new TransformStream(),
createWriteStream('output.json'),
)
Memory Leak Prevention
- Clean up event listeners in service shutdown hooks (
onModuleDestroy) - Use
WeakMap/WeakReffor caches that should not prevent garbage collection - Set explicit timeouts on all external connections (HTTP, DB, Redis)
- Monitor memory usage in production; set container memory limits
Refactoring Techniques
Basic Policy
- Small Steps: Maintain always-working state through gradual improvements
- Safe Changes: Minimize the scope of changes at once
- Behavior Guarantee: Ensure existing behavior remains unchanged while proceeding
Implementation Procedure: Understand Current State → Gradual Changes → Behavior Verification → Final Validation
Priority: Duplicate Code Removal > Large Function Division > Complex Conditional Branch Simplification > Type Safety Improvement
Performance Optimization
- Database Queries: Use indexes, avoid N+1 queries, paginate large result sets
- Caching: Apply at service layer — Redis for shared state, in-memory for single-instance
- Connection Pooling: Configure appropriate pool sizes for database and Redis connections
- Lazy Loading: Load expensive resources on demand, not at startup
- Batch Processing: Process bulk operations in batches to control memory usage