nestjs backend

NestJS Backend Expert Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nestjs backend" with this command: npx skills add ytl-cement/coding-buddy/ytl-cement-coding-buddy-nestjs-backend

NestJS Backend Expert Skill

This skill defines the standards for backend development using NestJS. It covers modular architecture, defensive coding, structured logging, testability, security hardening, performance optimization, scalability, documentation, and an iterative development workflow.

Skill Level: Level 2 (Standards + Mandatory Workflow + Automation Scripts)

Scripts

This skill includes automation scripts in scripts/ :

  • scaffold-module.sh — Generates a new NestJS module with the correct folder structure

  • check-structure.py — Validates that all modules comply with the required structure

  • run-unit-tests.sh — Runs unit tests with coverage enforcement, module filtering, and TDD watch mode

  • run-api-tests.sh — Runs API/E2E tests with auto-server startup and error format validation

  1. Mandatory Development Workflow

THIS SECTION OVERRIDES ALL OTHERS. Before touching any code, you MUST follow these 4 steps in order. Skipping steps is NOT allowed.

┌──────────────────────────────────────────────────────┐ │ Step 1: CLARIFY → Understand before you build │ │ Step 2: PLAN → Design before you code │ │ Step 3: BUILD → Code iteratively with TDD │ │ Step 4: VERIFY → Prove it works before shipping │ └──────────────────────────────────────────────────────┘

Step 1: CLARIFY (Do NOT skip)

Before writing any code, identify and resolve ambiguity.

Actions:

  • List ALL assumptions you are making about the feature

  • Identify unknowns: database schema? third-party APIs? auth requirements?

  • Ask the user to confirm or correct each assumption

  • Do NOT proceed to Step 2 until all assumptions are confirmed

Output: A list of confirmed requirements and decisions.

Example:

Clarification for: User Registration Feature

Assumptions (Please confirm ✅ or correct ❌):

  1. Users register with email + password (no social login) → ✅/❌
  2. Email must be unique across the system → ✅/❌
  3. Password minimum length is 8 characters → ✅/❌
  4. A welcome email is sent after registration → ✅/❌
  5. No email verification required for MVP → ✅/❌

Questions:

  • Should we support soft delete or hard delete for users?
  • Is there a rate limit on registration attempts?

Step 2: PLAN

After clarification, design the solution before coding.

Actions:

  • Break the feature into phases (see Section 12.2)

  • Define completion criteria (see Section 12.3)

  • Identify which existing modules/services are affected

  • List new files to be created (use scaffold-module.sh if creating a new module)

  • Present the plan to the user for approval

  • Do NOT proceed to Step 3 until the plan is approved

Output: A phased implementation plan with completion criteria.

Example:

Plan: User Registration Feature

Phase 1: Data Layer

  • Create User entity with email, hashedPassword, createdAt
  • Add unique index on email column
  • Generate migration

Phase 2: Business Logic

  • Create UserService.register() method
  • Hash password with bcrypt (salt round 12)
  • Check for duplicate email → throw ConflictException

Phase 3: API Layer

  • POST /auth/register endpoint
  • CreateUserDto with validation
  • Return 201 with user (exclude password)

Phase 4: Testing

  • Unit test: password hashing, duplicate email check
  • E2E test: successful registration, validation errors, duplicate email

Completion Criteria

  • POST /auth/register returns 201 on success
  • Duplicate email returns 409
  • Password is hashed (never stored in plain text)
  • Unit tests >80% coverage
  • E2E tests for happy path + error cases

Step 3: BUILD

Now you may write code. Follow these rules:

  • Use TDD — Follow the TDD-first cycle (Section 12.1)

  • Build in phases — Complete each phase before moving to the next

  • Scaffold first — Run scaffold-module.sh for new modules

  • Follow all standards — Sections 1–12 apply during this step

  • Commit after each phase — Small, atomic commits (Section 12.5)

Step 4: VERIFY

After building, prove the work is complete.

Checklist:

  • All unit tests passing (npm run test)
  • All e2e tests passing (npm run test:e2e)
  • Coverage meets threshold (npm run test:cov)
  • No linter errors (npm run lint)
  • Completion criteria from Step 2 are ALL met
  • Documentation updated (TSDoc + README if complex)
  • Error responses follow standard format (Section 10)
  • Logging added for key operations (Section 3)

Only after ALL boxes are checked is the feature considered DONE.

  1. Project Structure & Modularization

Adhere to a modular architecture where each feature is encapsulated within its own module.

Directory Structure

src/ ├── common/ # Shared resources (filters, guards, interceptors, pipes) │ ├── decorators/ │ ├── filters/ │ ├── guards/ │ ├── interceptors/ │ └── pipes/ ├── config/ # Configuration files (environment variables validation) ├── modules/ # Feature modules │ └── [feature-name]/ │ ├── dto/ # Data Transfer Objects (Validation layers) │ ├── entities/ # Database entities/models │ ├── interfaces/ # TypeScript interfaces │ ├── [feature].controller.ts │ ├── [feature].service.ts │ ├── [feature].module.ts │ ├── [feature].controller.spec.ts # Controller tests │ └── [feature].service.spec.ts # Service tests ├── app.module.ts # Root module └── main.ts # Application entry point

Module Rules

  • Encapsulation: Each feature (e.g., Users , Auth , Orders ) must have its own directory in modules/ .

  • Imports: Use the imports array in Module decorators to manage dependencies between modules. Avoid circular dependencies.

  • Barrel Exports: Each module directory should have an index.ts barrel file exporting public-facing components.

  1. Defensive Coding & Validation

Prevent invalid data from entering the system and handle errors gracefully.

Core Principle: Validate at the Boundary, Trust Internally

Do NOT validate in every function. Only validate where untrusted data enters the system (the "trust boundary"). Once data passes validation at the boundary, internal functions should trust it. Over-validating causes performance waste and code bloat.

UNTRUSTED ZONE TRUSTED ZONE ───────────────────────────────────────────────── Client → [Controller/DTO] → Service → Repository → DB ▲ │ VALIDATE HERE ONLY ← Trust everything past this point

When to validate:

Data Source Validate? Method

User/Client input (HTTP requests) ✅ Always DTOs + class-validator at Controller layer

Between your own internal functions ❌ No Trust the caller — data was already validated

Response from an external API/service ✅ Yes Manual type/range checks in the service

Business rules (e.g., "does user exist?") ✅ Yes Service-level checks (not format validation)

Shared/reusable utility functions ✅ Light Simple if guards for misuse prevention

Exception 1 — Business Logic Validation (in Services):

// ✅ This is a business rule, NOT input format validation async createOrder(dto: CreateOrderDto) { const user = await this.userRepo.findOne({ where: { id: dto.userId } }); if (!user) throw new NotFoundException('User not found');

if (user.credit < dto.totalAmount) { throw new BadRequestException('Insufficient credit'); } }

Exception 2 — External API Responses (Another Trust Boundary):

// ✅ External API data is untrusted — validate it async getExchangeRate(): Promise<number> { const response = await this.httpService.get('https://api.rates.com/usd'); const rate = response.data?.rate; if (typeof rate !== 'number' || rate <= 0) { throw new InternalServerErrorException('Invalid exchange rate from provider'); } return rate; }

Exception 3 — Shared Utility Functions:

// ✅ Shared utility — multiple callers may misuse it export function calculateDiscount(price: number, percentage: number): number { if (price < 0 || percentage < 0 || percentage > 100) { throw new Error('Invalid discount parameters'); } return price * (1 - percentage / 100); }

DTOs Enable Strict Validation

  • Always use Data Transfer Objects (DTOs) for all network requests.

  • Use class-validator and class-transformer decorators.

// create-user.dto.ts import { IsString, IsEmail, MinLength, IsNotEmpty } from 'class-validator';

export class CreateUserDto { @IsString() @IsNotEmpty() name: string;

@IsEmail() email: string;

@IsString() @MinLength(8) password: string; }

Global Validation Pipe

Enable global validation in main.ts with whitelisting to strip unknown properties (security best practice).

// main.ts app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }));

Exception Handling

  • Never let the application crash.

  • Use Global Exception Filters to catch unhandled errors and return standardized JSON responses.

  • Throw specific HTTP exceptions (e.g., NotFoundException , BadRequestException ) instead of generic errors.

  1. Logging & Observability

Maintain visibility into application behavior for debugging and monitoring.

Logging Standards

  • Use the built-in Logger service or a structured logger (e.g., winston , pino ).

  • Log context (class name) to trace the source of logs.

  • Never log sensitive data (passwords, tokens, PII).

private readonly logger = new Logger(UserService.name);

async createUser(dto: CreateUserDto) { this.logger.log(Creating user with email: ${dto.email}); try { // ... business logic } catch (error) { this.logger.error(Failed to create user, error.stack); throw error; } }

Logging Interceptor

Implement a global interceptor to log every incoming request and its duration.

// logging.interceptor.ts @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('HTTP');

intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const req = context.switchToHttp().getRequest(); const { method, url } = req; const now = Date.now();

return next.handle().pipe(
  tap(() => this.logger.log(`${method} ${url} ${Date.now() - now}ms`)),
);

} }

  1. Unit Testing Strategy

Build testable applications by design.

Testing Principles

  • Isolation: Unit tests must test a single class/function in isolation. Mock all dependencies.

  • Dependency Injection: Leverage NestJS DI system for testing.

  • Coverage: Aim for high coverage on business logic (Services), not just boilerplate (Modules).

Writing Tests

  • Create *.spec.ts files alongside the source files.

  • Use Test.createTestingModule to compile a module with mocks.

// user.service.spec.ts describe('UserService', () => { let service: UserService; let repository: Repository<User>;

beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), useValue: { save: jest.fn(), findOne: jest.fn(), }, }, ], }).compile();

service = module.get&#x3C;UserService>(UserService);
repository = module.get(getRepositoryToken(User));

});

it('should be defined', () => { expect(service).toBeDefined(); });

it('should create a user', async () => { const dto = { name: 'Test', email: 'test@test.com' }; await service.create(dto); expect(repository.save).toHaveBeenCalledWith(dto); }); });

  1. Documentation & Comments

Clear documentation is essential for maintaining complex systems and onboarding new developers (or AI agents).

Code Comments (TSDoc)

  • Use TSDoc format /** ... */ for all public classes, methods, and interfaces.

  • Complex Logic: If a method is complex, explain the "why" and "how" within the method body using // comments.

/**

  • Calculates the total price including tax and discounts.
  • @param items - List of items in the cart
  • @param discountCode - Optional discount code
  • @returns Final price calculation result */ calculateTotal(items: Item[], discountCode?: string): number { // We apply the tax after the discount is deducted to ensure... ... }

Module READMEs

  • For highly complex modules (e.g., a custom payment engine, or a complex state machine), you MUST create a README.md inside that module's directory.

  • This README.md should explain:

  • Purpose: What does this module do?

  • Data Flow: How does data enter and exit?

  • Key Components: Which classes are the "brain" of the module?

  • Dependencies: What other modules does it rely on?

Architecture Diagrams

  • For complex flows, include Mermaid diagrams in the module's README.md .

graph TD A[Client] -->|Request| B(Controller) B -->|Validate| C{Service} C -->|Success| D[Database] C -->|Fail| E[Error Handler]

  1. Security Hardening

Every backend MUST implement these security measures. Security is not optional.

HTTP Security Headers (Helmet)

Always register helmet in main.ts to set secure HTTP headers (X-Frame-Options, CSP, etc.).

// main.ts import helmet from 'helmet';

async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(helmet()); // ... }

CORS Configuration

Configure strict CORS. Never use origin: '*' in production.

app.enableCors({ origin: configService.get<string>('ALLOWED_ORIGINS').split(','), methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], credentials: true, });

Rate Limiting

Use @nestjs/throttler to protect against brute-force and DDoS.

// app.module.ts import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

@Module({ imports: [ ThrottlerModule.forRoot([{ ttl: 60000, // 60 seconds limit: 100, // 100 requests per ttl window }]), ], providers: [ { provide: APP_GUARD, useClass: ThrottlerGuard }, ], }) export class AppModule {}

Authentication & Authorization

  • Always use JWT-based authentication via @nestjs/passport
  • @nestjs/jwt .
  • Use Guards for role-based access control (RBAC).

// roles.guard.ts @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) return true;

const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));

} }

Data Sanitization

  • Sanitize inputs to prevent XSS: strip HTML tags from string inputs.

  • Prevent SQL Injection: always use parameterized queries or an ORM (TypeORM, Prisma). Never concatenate raw SQL.

  • Hash passwords: Always use bcrypt with a salt round of 10+.

import * as bcrypt from 'bcrypt';

const hashedPassword = await bcrypt.hash(dto.password, 12);

  1. Performance Optimization

Design for speed. Every millisecond counts in production.

Caching

Use @nestjs/cache-manager with Redis for frequently read data.

// user.service.ts import { CACHE_MANAGER } from '@nestjs/cache-manager';

@Injectable() export class UserService { constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

async findOne(id: string): Promise<User> { const cached = await this.cacheManager.get<User>(user:${id}); if (cached) return cached;

const user = await this.userRepository.findOne({ where: { id } });
await this.cacheManager.set(`user:${id}`, user, 300_000); // 5 min TTL
return user;

} }

Pagination

Never return unbounded lists. Always enforce pagination.

// pagination.dto.ts export class PaginationDto { @IsOptional() @Type(() => Number) @IsInt() @Min(1) page: number = 1;

@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) // Hard cap to prevent abuse limit: number = 20; }

// In service async findAll(pagination: PaginationDto) { const { page, limit } = pagination; const [items, total] = await this.repo.findAndCount({ skip: (page - 1) * limit, take: limit, }); return { items, total, page, limit, totalPages: Math.ceil(total / limit) }; }

Database Optimization

  • Indexes: Always define indexes on columns used in WHERE , ORDER BY , and JOIN clauses.

  • Select: Only select columns you need. Avoid SELECT * .

  • Relations: Use eager loading sparingly. Prefer explicit relations in queries.

// user.entity.ts @Entity() export class User { @Index() // Add index for frequently queried fields @Column({ unique: true }) email: string; }

Response Compression

Enable gzip compression to reduce payload sizes.

// main.ts import compression from 'compression'; app.use(compression());

Global Performance Benchmarking (MANDATORY)

To guarantee high performance and avoid server degradation, you MUST implement a global BenchmarkInterceptor on all new NestJS projects. This interceptor explicitly measures execution time and final payload size, enforcing strict guardrails.

Guardrails:

  • Execution Time: Warn if request takes > 2000ms .

  • Payload Size: Warn if JSON payload > 100kb (approx 102,400 characters).

// benchmark.interceptor.ts import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators';

@Injectable() export class BenchmarkInterceptor implements NestInterceptor { private readonly logger = new Logger('PerformanceBenchmark'); private readonly MAX_EXECUTION_MS = 2000; private readonly MAX_PAYLOAD_BYTES = 102400; // 100kb

intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const req = context.switchToHttp().getRequest(); const { method, url } = req; const now = Date.now();

return next.handle().pipe(
  tap((data) => {
    const executionTime = Date.now() - now;
    
    // 1. Time Benchmark Segment
    if (executionTime > this.MAX_EXECUTION_MS) {
       this.logger.warn(
         `[CRITICAL] SLOW EXECUTION ${method} ${url} - Took ${executionTime}ms (Limit: ${this.MAX_EXECUTION_MS}ms). Needs immediate optimization or background processing.`
       );
    }

    // 2. Payload Benchmark Segment
    if (data) {
      // Note: stringify length is a rough estimate of bytes sent over wire
      const payloadSize = JSON.stringify(data).length;
      if (payloadSize > this.MAX_PAYLOAD_BYTES) {
        this.logger.warn(
          `[CRITICAL] LARGE PAYLOAD ${method} ${url} - Size ${payloadSize} bytes (Limit: ${this.MAX_PAYLOAD_BYTES}). Implement server-side pagination or omit unnecessary nested relations.`
        );
      }
    }
  }),
);

} }

Registration: Register this interceptor globally in main.ts so it applies to every single route.

// main.ts app.useGlobalInterceptors(new BenchmarkInterceptor());

Lazy Loading Modules

Use LazyModuleLoader for heavy modules not needed at startup.

const { ReportModule } = await import('./modules/report/report.module'); const moduleRef = await this.lazyModuleLoader.load(() => ReportModule);

Algorithmic Complexity (Big-O Rules)

NEVER use nested loops on arrays. O(n²) time complexity is unacceptable for production code. Use Map or Set for O(1) lookups instead.

❌ BAD — O(n²) nested loop:

// Finding matching users between two lists function findMatches(usersA: User[], usersB: User[]): User[] { const matches: User[] = []; for (const a of usersA) { // O(n) for (const b of usersB) { // × O(n) = O(n²) 💀 if (a.email === b.email) { matches.push(a); } } } return matches; }

✅ GOOD — O(n) with Set:

function findMatches(usersA: User[], usersB: User[]): User[] { const emailSet = new Set(usersB.map(u => u.email)); // O(n) build return usersA.filter(u => emailSet.has(u.email)); // O(n) lookup // Total: O(n) ✅ }

Quick Reference:

Pattern Time Use When

Single loop O(n) ✅ Always preferred

Map /Set lookup O(1) ✅ Matching, deduplication, grouping

Nested loop O(n²) ❌ NEVER — refactor to Map/Set

.find() inside .map()

O(n²) ❌ Hidden nested loop — use Map

Array .includes() in loop O(n²) ❌ Hidden nested loop — use Set

Grouping pattern (Map-based):

// ❌ BAD: Grouping with nested filter const groupedByRole = roles.map(role => users.filter(u => u.role === role) // O(n) per role = O(n × roles) );

// ✅ GOOD: Grouping with Map (single pass) const groupedByRole = new Map<string, User[]>(); for (const user of users) { const group = groupedByRole.get(user.role) || []; group.push(user); groupedByRole.set(user.role, group); }

Batch / Chunk Processing

Never load or process unbounded datasets in a single operation. Process in chunks to control memory and avoid DB timeouts.

/**

  • Processes records in chunks to avoid memory overflow.
  • @param chunkSize - Number of records per batch (default: 500) */ async processAllOrders(chunkSize = 500): Promise<void> { let offset = 0; let hasMore = true;

while (hasMore) { const chunk = await this.orderRepo.find({ skip: offset, take: chunkSize, });

if (chunk.length === 0) {
  hasMore = false;
  break;
}

// Process this chunk
await Promise.all(chunk.map(order => this.processOrder(order)));

this.logger.log(`Processed ${offset + chunk.length} orders`);
offset += chunkSize;

// Safety: if chunk is smaller than chunkSize, we've reached the end
if (chunk.length &#x3C; chunkSize) hasMore = false;

} }

Bulk Insert/Update:

// ❌ BAD: Inserting one by one in a loop for (const dto of dtos) { await this.repo.save(dto); // N separate INSERT queries 💀 }

// ✅ GOOD: Bulk insert in one query await this.repo.save(dtos); // Single INSERT with multiple values ✅

// ✅ GOOD: Bulk insert in chunks (for very large datasets) for (let i = 0; i < dtos.length; i += 500) { const chunk = dtos.slice(i, i + 500); await this.repo.save(chunk); }

N+1 Query Prevention

The N+1 problem occurs when you query a list (1 query) then loop through it to fetch related data (N queries). This is one of the most common performance killers.

❌ BAD — N+1 problem:

// 1 query to get all orders const orders = await this.orderRepo.find();

// N queries to get each user 💀 for (const order of orders) { order.user = await this.userRepo.findOne({ where: { id: order.userId } }); }

✅ GOOD — Eager join (1 query):

// Single query with JOIN const orders = await this.orderRepo.find({ relations: ['user'], // LEFT JOIN in one query ✅ });

✅ GOOD — QueryBuilder for complex joins:

const orders = await this.orderRepo .createQueryBuilder('order') .leftJoinAndSelect('order.user', 'user') .leftJoinAndSelect('order.items', 'item') .where('order.status = :status', { status: 'active' }) .select(['order.id', 'order.total', 'user.name', 'item.productName']) .getMany();

✅ GOOD — Batch lookup with Map (when JOIN is not possible):

const orders = await this.orderRepo.find();

// Collect all unique user IDs const userIds = [...new Set(orders.map(o => o.userId))];

// Single query with IN clause const users = await this.userRepo.findByIds(userIds); const userMap = new Map(users.map(u => [u.id, u]));

// O(1) lookup per order for (const order of orders) { order.user = userMap.get(order.userId); }

Memory Management

Prevent memory leaks and out-of-memory crashes in production.

Use Streams for large data:

// ❌ BAD: Load entire CSV into memory const fileContent = fs.readFileSync('large-file.csv', 'utf8'); const rows = fileContent.split('\n'); // 500MB string in memory 💀

// ✅ GOOD: Stream line by line import { createReadStream } from 'fs'; import * as readline from 'readline';

async processLargeFile(filePath: string): Promise<void> { const stream = createReadStream(filePath); const rl = readline.createInterface({ input: stream });

for await (const line of rl) { await this.processLine(line); // One line at a time ✅ } }

Request body size limits:

// main.ts — Prevent oversized payloads from crashing the server app.use(json({ limit: '10mb' })); app.use(urlencoded({ limit: '10mb', extended: true }));

Clean up resources on shutdown:

@Injectable() export class CleanupService implements OnModuleDestroy { private intervals: NodeJS.Timeout[] = [];

registerInterval(interval: NodeJS.Timeout) { this.intervals.push(interval); }

onModuleDestroy() { this.intervals.forEach(i => clearInterval(i)); this.intervals = []; } }

Async Concurrency Patterns

Use concurrent execution for independent async operations. Sequential await wastes time.

❌ BAD — Sequential (total: sum of all times):

async getDashboard(userId: string) { const user = await this.userService.findOne(userId); // 50ms const orders = await this.orderService.findByUser(userId); // 100ms const stats = await this.statsService.getForUser(userId); // 80ms // Total: 230ms 💀 (each waits for the previous)

return { user, orders, stats }; }

✅ GOOD — Parallel with Promise.all (total: max of all times):

async getDashboard(userId: string) { const [user, orders, stats] = await Promise.all([ this.userService.findOne(userId), // 50ms ─┐ this.orderService.findByUser(userId), // 100ms ─┤ Run in parallel this.statsService.getForUser(userId), // 80ms ─┘ ]); // Total: 100ms ✅ (only waits for the slowest)

return { user, orders, stats }; }

✅ Promise.allSettled — When partial failure is acceptable:

async sendNotifications(userIds: string[]) { const results = await Promise.allSettled( userIds.map(id => this.notificationService.send(id)) );

const failed = results.filter(r => r.status === 'rejected'); if (failed.length > 0) { this.logger.warn(${failed.length}/${userIds.length} notifications failed); } }

Performance Anti-Patterns Quick Reference

A cheat sheet of common mistakes. If you see any of these in your code, immediately refactor.

❌ Anti-Pattern ✅ Fix Why

Nested for loops on arrays Use Map /Set for lookups O(n²) → O(n)

.find() inside .map() or .forEach()

Pre-build a Map , then .get()

Hidden O(n²)

Array.includes() inside a loop Convert to Set , use .has()

O(n²) → O(n)

await inside a for loop (independent calls) Promise.all()

Sequential → parallel

SELECT * in queries Select only needed columns Wastes bandwidth + memory

No pagination on list endpoints Enforce PaginationDto (max 100) Unbounded = potential OOM

Loading whole file into memory Use createReadStream()

OOM on large files

Individual INSERT in a loop repo.save(arrayOfEntities)

N queries → 1 query

Fetching relations in a loop (N+1) Use relations: [...] or JOIN N+1 → 1 query

No caching on hot read paths @nestjs/cache-manager

  • Redis DB thrashing

JSON.parse() on huge strings Use streaming JSON parser Blocks event loop

Synchronous file operations (fs.readFileSync ) Use async fs.promises

Blocks event loop

  1. Scalability & Resilience

Design systems that survive failures and scale horizontally.

Health Checks

Use @nestjs/terminus for load balancer and Kubernetes readiness probes.

// health.controller.ts @Controller('health') export class HealthController { constructor( private health: HealthCheckService, private db: TypeOrmHealthIndicator, private memory: MemoryHealthIndicator, ) {}

@Get() check() { return this.health.check([ () => this.db.pingCheck('database'), () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB ]); } }

Queue-Based Processing

Offload heavy work (email sending, PDF generation, data processing) to background queues.

// Using @nestjs/bull @Processor('email') export class EmailProcessor { private readonly logger = new Logger(EmailProcessor.name);

@Process('send') async handleSend(job: Job<{ to: string; subject: string; body: string }>) { this.logger.log(Sending email to ${job.data.to}); await this.mailService.send(job.data); }

@OnQueueFailed() handleFailure(job: Job, error: Error) { this.logger.error(Job ${job.id} failed: ${error.message}, error.stack); } }

Circuit Breaker Pattern

Protect your app from cascading failures when calling external APIs.

/**

  • Wraps an external API call with circuit breaker logic.
  • After 3 consecutive failures, the circuit opens for 30 seconds,
  • rejecting requests immediately instead of waiting for timeouts. */ @Injectable() export class ExternalApiService { private failures = 0; private circuitOpen = false; private readonly THRESHOLD = 3; private readonly COOLDOWN_MS = 30_000;

async callExternalApi<T>(fn: () => Promise<T>): Promise<T> { if (this.circuitOpen) { throw new ServiceUnavailableException('Circuit is open. Try again later.'); } try { const result = await fn(); this.failures = 0; // Reset on success return result; } catch (error) { this.failures++; if (this.failures >= this.THRESHOLD) { this.circuitOpen = true; setTimeout(() => { this.circuitOpen = false; this.failures = 0; }, this.COOLDOWN_MS); } throw error; } } }

Graceful Shutdown

Handle SIGTERM to finish in-flight requests before exiting. Critical for container deployments.

// main.ts app.enableShutdownHooks();

// In a service @Injectable() export class AppService implements OnApplicationShutdown { private readonly logger = new Logger(AppService.name);

onApplicationShutdown(signal: string) { this.logger.warn(Application shutting down (signal: ${signal})); // Close DB connections, flush logs, finish queue jobs, etc. } }

Stateless Design

  • Never store session state in application memory.

  • Use Redis for session storage, caching, and shared state.

  • This allows horizontal scaling (running multiple instances behind a load balancer).

  1. Configuration Management

Fail fast on bad configuration. Never read raw process.env .

Environment Validation

Validate all environment variables at startup using Joi .

// config/configuration.ts import * as Joi from 'joi';

export const validationSchema = Joi.object({ NODE_ENV: Joi.string().valid('development', 'staging', 'production').required(), PORT: Joi.number().default(3000), DATABASE_URL: Joi.string().uri().required(), JWT_SECRET: Joi.string().min(32).required(), REDIS_URL: Joi.string().uri().required(), ALLOWED_ORIGINS: Joi.string().required(), });

ConfigModule Setup

Register globally and inject via ConfigService .

// app.module.ts @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validationSchema, envFilePath: .env.${process.env.NODE_ENV || 'development'}, }), ], }) export class AppModule {}

Usage in Services

Never use process.env directly. Always inject ConfigService .

@Injectable() export class AuthService { constructor(private configService: ConfigService) {}

getJwtSecret(): string { return this.configService.getOrThrow<string>('JWT_SECRET'); } }

  1. Standardized Error Responses

Every error must follow a consistent format for frontend consumption and debugging.

Error Response Shape

All errors returned to clients must follow this structure:

{ "statusCode": 400, "error": "Bad Request", "message": "Validation failed", "details": [ { "field": "email", "message": "email must be a valid email" } ], "correlationId": "abc-123-def-456", "timestamp": "2026-02-17T09:00:00.000Z", "path": "/api/users" }

Global Exception Filter

Catches all unhandled exceptions and formats them consistently.

// all-exceptions.filter.ts @Catch() export class AllExceptionsFilter implements ExceptionFilter { private readonly logger = new Logger(AllExceptionsFilter.name);

catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>();

const status = exception instanceof HttpException
  ? exception.getStatus()
  : HttpStatus.INTERNAL_SERVER_ERROR;

const message = exception instanceof HttpException
  ? exception.getResponse()
  : 'Internal Server Error';

const errorResponse = {
  statusCode: status,
  error: HttpStatus[status],
  message: typeof message === 'string' ? message : (message as any).message,
  details: typeof message === 'object' ? (message as any).message : undefined,
  correlationId: request.headers['x-correlation-id'] || crypto.randomUUID(),
  timestamp: new Date().toISOString(),
  path: request.url,
};

// Log server errors with full stack trace
if (status >= 500) {
  this.logger.error(
    `${request.method} ${request.url} ${status}`,
    exception instanceof Error ? exception.stack : String(exception),
  );
}

response.status(status).json(errorResponse);

} }

Correlation IDs

Attach a unique ID to every request for end-to-end tracing across services.

// correlation-id.middleware.ts @Injectable() export class CorrelationIdMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const correlationId = req.headers['x-correlation-id'] as string || crypto.randomUUID(); req.headers['x-correlation-id'] = correlationId; res.setHeader('x-correlation-id', correlationId); next(); } }

  1. Expanded Testing Strategy

A robust backend requires multiple layers of testing.

Testing Pyramid

   /  E2E  \         Few, slow, high-confidence
  /----------\
 / Integration \      Moderate amount
/----------------\

/ Unit Tests \ Many, fast, isolated /____________________\

E2E (End-to-End) Tests

Test the full HTTP lifecycle using supertest .

// test/users.e2e-spec.ts describe('UsersController (e2e)', () => { let app: INestApplication;

beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ imports: [AppModule], }).compile();

app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();

});

afterAll(async () => { await app.close(); });

it('POST /users - should create a user', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'John', email: 'john@test.com', password: 'securePass1' }) .expect(201) .expect((res) => { expect(res.body).toHaveProperty('id'); expect(res.body.email).toBe('john@test.com'); }); });

it('POST /users - should reject invalid email', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'John', email: 'invalid', password: 'securePass1' }) .expect(400); }); });

Test Coverage Thresholds

Enforce minimum coverage in jest.config.ts :

// jest.config.ts export default { coverageThreshold: { global: { branches: 70, functions: 80, lines: 80, statements: 80, }, }, };

Test Naming Convention

Follow this naming pattern for clarity in test reports:

should [expected behavior] when [condition]

Examples:

  • should return 404 when user does not exist

  • should hash password when creating a new user

  • should throw ForbiddenException when user lacks admin role

Test Database

  • For unit tests: Mock repositories (as shown in Section 4).

  • For e2e tests: Use a separate test database or an in-memory SQLite instance.

  • Never run tests against a production or staging database.

// test/test-database.module.ts @Module({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User, Order], synchronize: true, // OK for test only }), ], }) export class TestDatabaseModule {}

  1. Iterative Development Workflow

Adopt an iteration-first mindset. Don't aim for perfection on the first attempt — let structured loops refine the work. Failures are data, not dead ends.

Core Principles

Principle Meaning

Iteration > Perfection Ship a working version fast, then improve iteratively

Failures Are Data Every failed test or error tells you specifically what to fix

Clear Criteria First Define "done" as a measurable checklist BEFORE writing code

Persistence Wins Keep the cycle going until all criteria are met

12.1 TDD-First Development Cycle

Every new feature MUST follow this cycle:

┌─────────────────────────────────────────────┐ │ 1. Write a failing test │ │ 2. Write minimal code to make it pass │ │ 3. Run tests │ │ 4. ❌ Red? → Debug and fix → Go to step 3 │ │ 5. ✅ Green? → Refactor if needed │ │ 6. Repeat for the next requirement │ └─────────────────────────────────────────────┘

// Example: Building a UserService.create() method

// STEP 1: Write the failing test FIRST it('should hash password when creating a new user', async () => { const dto = { name: 'Test', email: 'test@test.com', password: 'plaintext' }; const result = await service.create(dto); expect(result.password).not.toBe('plaintext'); expect(result.password).toMatch(/^$2[ab]$/); // bcrypt hash pattern });

// STEP 2: Run test → it FAILS (create() doesn't hash yet) // STEP 3: Implement the minimal code to pass async create(dto: CreateUserDto): Promise<User> { dto.password = await bcrypt.hash(dto.password, 12); return this.userRepository.save(dto); }

// STEP 4: Run test → it PASSES ✅ // STEP 5: Refactor if needed, then move to next requirement

12.2 Phased Feature Implementation

Never build an entire feature in one shot. Break it into sequential phases, each with its own completion criteria.

Phase Template:

Phase 1: Data Layer

  • Entity/model created with proper indexes
  • Migration generated and tested
  • Repository patterns defined

Phase 2: Business Logic

  • Service created with all methods
  • Business rules validated (Section 2 exceptions)
  • Error cases handled

Phase 3: API Layer

  • Controller with all endpoints
  • DTOs with validation decorators
  • Swagger/OpenAPI documentation

Phase 4: Testing & Verification

  • Unit tests for service (>80% coverage)
  • E2E tests for controller endpoints
  • Edge cases and error paths tested

Phase 5: Documentation

  • TSDoc on all public methods
  • Module README if complex (Section 5)
  • CHANGELOG updated

Rule: Do NOT start Phase N+1 until Phase N's checklist is fully complete.

12.3 Clear Completion Criteria

Before writing a single line of code, define what "done" looks like. Use this template:

Feature: [Feature Name]

Success Criteria

  • All CRUD endpoints working and returning correct status codes
  • Input validation in place (DTOs with class-validator)
  • Authentication/authorization enforced on protected routes
  • Unit tests passing with >80% coverage on service layer
  • E2E tests covering happy path + error cases
  • No linter errors (npm run lint)
  • Documentation updated (TSDoc + README if complex)
  • Logging added for key operations (Section 3)
  • Error responses follow standard format (Section 10)

12.4 Self-Healing Bug Fix Workflow

When a bug is found, follow this structured approach:

Step 1: Reproduce → Write a failing test that captures the exact bug behavior

Step 2: Root Cause → Trace the data flow to identify WHERE and WHY it fails

Step 3: Fix → Implement the smallest possible fix

Step 4: Regression Test → Ensure the failing test now passes → Ensure ALL existing tests still pass

Step 5: Verify → Run the full test suite → Manual verification if needed

Step 6: Escalate (if stuck after 3 attempts) → Document what was tried → List potential root causes → Suggest alternative approaches

Critical Rule: Never fix a bug without writing a regression test first. The test proves the bug existed and prevents it from returning.

12.5 Safe Refactoring Protocol

Refactoring must NEVER break existing behavior. Follow this protocol:

Pre-Refactor: ✅ All existing tests passing ✅ Current coverage documented

During Refactor: 🔄 Make ONE structural change at a time 🔄 Run tests after EACH change 🔄 Commit after each successful change (small, atomic commits)

Post-Refactor: ✅ All tests still passing ✅ Coverage is equal or higher ✅ No new linter warnings ✅ Code review / diff review

// Example commit history for a safe refactoring: // commit 1: "refactor(users): extract password hashing to utility" // commit 2: "refactor(users): replace inline validation with DTO pipe" // commit 3: "refactor(users): move email logic to dedicated service" // Each commit is independently revertible if something breaks.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

sdapp-commit

No summary provided by upstream source.

Repository SourceNeeds Review
General

sdapp-jira-log

No summary provided by upstream source.

Repository SourceNeeds Review
General

unit-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

debugging

No summary provided by upstream source.

Repository SourceNeeds Review