Тема 9. Валідація даних та конвеєр обробки запитів у NestJS

Pipeline Best Practices: найкращі практики

Single Responsibility, performance, error handling, testing patterns

Pipeline Best Practices: найкращі практики

🎯 Мета лекції

  • Зрозуміти принцип Single Responsibility для компонентів Pipeline
  • Опанувати patterns оптимізації performance (кешування, lazy loading, async operations)
  • Навчитися проектувати ефективні стратегії обробки помилок у Pipeline
  • Вивчити best practices тестування Guards, Interceptors, Pipes, Filters
  • Засвоїти архітектурні patterns для складних Pipeline (multi-tenant, microservices)
  • Практикувати debugging та моніторинг компонентів Pipeline
  • Розуміти trade-offs між глобальною та локальною реєстрацією компонентів

🔑 Ключові терміни

  • Single Responsibility Principle (принцип єдиної відповідальності): кожен компонент має одну чітко визначену задачу
  • Performance Optimization (оптимізація продуктивності): мінімізація overhead компонентів Pipeline
  • Error Handling Strategy (стратегія обробки помилок): централізований підхід до обробки виключень
  • Testing Pyramid (піраміда тестування): співвідношення unit/integration/e2e тестів
  • Pipeline Composition (композиція Pipeline): комбінування компонентів для складної логіки
  • Observability (спостережуваність): моніторинг, логування, трейсинг компонентів Pipeline

Single Responsibility Principle у Pipeline

Кожен компонент Pipeline має виконувати одну чітко визначену задачу:

Приклад порушення SRP

@Injectable()
export class MegaGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    
    // 1. Аутентифікація
    const token = request.headers.authorization;
    if (!token) return false;
    const user = await this.jwtService.verify(token);
    
    // 2. Авторизація
    const requiredRoles = this.reflector.get('roles', context.getHandler());
    if (!requiredRoles.some(role => user.roles.includes(role))) {
      return false;
    }
    
    // 3. Rate limiting
    const key = `${user.id}:${request.url}`;
    const count = await this.redisClient.get(key);
    if (count > 10) return false;
    await this.redisClient.incr(key);
    
    // 4. Валідація ownership
    const resourceId = request.params.id;
    const resource = await this.resourceService.findOne(resourceId);
    if (resource.userId !== user.id) return false;
    
    request.user = user;
    return true;
  }
}

Переваги розділення:

  • Кожен Guard можна тестувати окремо
  • Переві використання у різних контекстах
  • Простіше підтримувати та модифікувати
  • Зрозуміліший порядок виконання

Приклад правильного розділення відповідальностей

// Interceptor: логування запитів
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    console.log(`${request.method} ${request.url}`);
    return next.handle();
  }
}

// Interceptor: трансформація відповідей
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    return next.handle().pipe(
      map(data => ({ data, timestamp: new Date().toISOString() }))
    );
  }
}

// Interceptor: кешування
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const key = this.generateCacheKey(context);
    const cached = this.cache.get(key);
    
    if (cached) return of(cached);
    
    return next.handle().pipe(tap(data => this.cache.set(key, data)));
  }
}

Performance Optimization: оптимізація продуктивності

1. Мінімізація async операцій у Guards

@Injectable()
export class ResourceGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    
    // 3 окремих DB queries
    const user = await this.usersService.findOne(request.user.id);
    const resource = await this.resourcesService.findOne(request.params.id);
    const permissions = await this.permissionsService.getForUser(user.id);
    
    return permissions.includes('resource:read');
  }
}

2. Кешування метаданих у Reflector

@Injectable()
export class OptimizedRolesGuard implements CanActivate {
  private metadataCache = new Map<Function, string[]>();

  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const handler = context.getHandler();
    
    // Кешування метаданих
    if (!this.metadataCache.has(handler)) {
      const roles = this.reflector.getAllAndOverride<string[]>('roles', [
        handler,
        context.getClass(),
      ]);
      this.metadataCache.set(handler, roles);
    }
    
    const requiredRoles = this.metadataCache.get(handler);
    if (!requiredRoles) return true;
    
    const user = context.switchToHttp().getRequest().user;
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

3. Lazy loading для важких операцій

@Injectable()
export class HeavyInterceptor implements NestInterceptor {
  private heavyService?: HeavyService;

  constructor(private moduleRef: ModuleRef) {}

  async intercept(context: ExecutionContext, next: CallHandler) {
    // Lazy load сервісу лише коли потрібно
    if (!this.heavyService) {
      this.heavyService = await this.moduleRef.get(HeavyService, { strict: false });
    }
    
    return next.handle();
  }
}

4. Умовне виконання компонентів

@Injectable()
export class ConditionalLoggingInterceptor implements NestInterceptor {
  constructor(private reflector: Reflector) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    const shouldLog = this.reflector.get('log', context.getHandler());
    
    // Пропуск якщо логування не потрібне
    if (!shouldLog) {
      return next.handle();
    }
    
    // Логування лише для позначених endpoints
    console.log('Logging...');
    return next.handle();
  }
}

5. Оптимізація Exception Filters

@Catch()
export class OptimizedExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(OptimizedExceptionFilter.name);

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

    // Асинхронне логування без блокування відповіді
    setImmediate(() => {
      this.logger.error('Exception occurred', {
        url: request.url,
        error: exception,
      });
    });

    // Швидка відповідь клієнту
    const status = exception instanceof HttpException 
      ? exception.getStatus() 
      : 500;
    
    response.status(status).json({
      statusCode: status,
      message: 'Error occurred',
    });
  }
}

Error Handling Strategy: стратегії обробки помилок

1. Централізована обробка через Exception Filters

// filters/all-exceptions.filter.ts
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(
    private logger: Logger,
    private errorReporter: ErrorReporterService,
  ) {}

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

    const status = this.getStatus(exception);
    const message = this.getMessage(exception);

    // Логування
    this.logger.error(`${request.method} ${request.url}`, {
      statusCode: status,
      message,
      stack: exception instanceof Error ? exception.stack : undefined,
      userId: request.user?.id,
    });

    // Відправлення у Sentry/Datadog
    if (status >= 500) {
      this.errorReporter.captureException(exception, { request });
    }

    // Відповідь клієнту
    response.status(status).json({
      statusCode: status,
      message: status >= 500 ? 'Internal server error' : message,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }

  private getStatus(exception: unknown): number {
    if (exception instanceof HttpException) {
      return exception.getStatus();
    }
    return HttpStatus.INTERNAL_SERVER_ERROR;
  }

  private getMessage(exception: unknown): string {
    if (exception instanceof HttpException) {
      return exception.message;
    }
    if (exception instanceof Error) {
      return exception.message;
    }
    return 'Unknown error';
  }
}

2. Ієрархія кастомних виключень

// Базове виключення
export class AppException extends HttpException {
  constructor(
    public readonly code: string,
    message: string,
    statusCode: number,
  ) {
    super({ code, message }, statusCode);
  }
}

// Специфічні виключення
export class UserNotFoundException extends AppException {
  constructor(userId: string) {
    super('USER_NOT_FOUND', `User with ID ${userId} not found`, 404);
  }
}

export class InsufficientBalanceException extends AppException {
  constructor(required: number, available: number) {
    super(
      'INSUFFICIENT_BALANCE',
      `Insufficient balance: required ${required}, available ${available}`,
      402
    );
  }
}

export class ResourceConflictException extends AppException {
  constructor(resourceType: string, identifier: string) {
    super(
      'RESOURCE_CONFLICT',
      `${resourceType} with identifier ${identifier} already exists`,
      409
    );
  }
}

Використання:

@Controller('users')
export class UsersController {
  @Get(':id')
  async findOne(@Param('id') id: string) {
    const user = await this.usersService.findById(id);
    
    if (!user) {
      throw new UserNotFoundException(id); // Клієнт отримає код помилки
    }
    
    return user;
  }

  @Post('purchase')
  async purchase(@Body() dto: PurchaseDto, @CurrentUser() user: User) {
    if (user.balance < dto.amount) {
      throw new InsufficientBalanceException(dto.amount, user.balance);
    }
    
    return this.paymentsService.process(dto);
  }
}

3. Circuit Breaker для зовнішніх API

@Injectable()
export class CircuitBreakerInterceptor implements NestInterceptor {
  private failures = new Map<string, number>();
  private readonly threshold = 5;
  private readonly timeout = 60000; // 1 minute

  intercept(context: ExecutionContext, next: CallHandler) {
    const key = this.getKey(context);
    const failures = this.failures.get(key) || 0;

    if (failures >= this.threshold) {
      throw new ServiceUnavailableException(
        'Service temporarily unavailable due to circuit breaker'
      );
    }

    return next.handle().pipe(
      tap(() => this.failures.set(key, 0)), // Reset on success
      catchError(err => {
        this.failures.set(key, failures + 1);
        
        setTimeout(() => {
          this.failures.set(key, 0); // Reset after timeout
        }, this.timeout);
        
        throw err;
      }),
    );
  }

  private getKey(context: ExecutionContext): string {
    return `${context.getClass().name}.${context.getHandler().name}`;
  }
}

Testing Patterns: патерни тестування

Testing Pyramid для Pipeline компонентів

         /\
        /E2\     10% - E2E tests (повні інтеграційні сценарії)
       /____\
      /      \
     / Integr \   20% - Integration tests (взаємодія компонентів)
    /________\
   /          \
  /   Unit     \  70% - Unit tests (окремі компоненти)
 /______________\

1. Unit-тестування Guards

import { Test } from '@nestjs/testing';
import { Reflector } from '@nestjs/core';
import { ExecutionContext } from '@nestjs/common';
import { RolesGuard } from './roles.guard';

describe('RolesGuard', () => {
  let guard: RolesGuard;
  let reflector: Reflector;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [RolesGuard, Reflector],
    }).compile();

    guard = module.get(RolesGuard);
    reflector = module.get(Reflector);
  });

  describe('canActivate', () => {
    it('should return true when no roles required', () => {
      jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);

      const context = createMockContext({ user: { roles: ['user'] } });
      
      expect(guard.canActivate(context)).toBe(true);
    });

    it('should return false when user lacks required role', () => {
      jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);

      const context = createMockContext({ user: { roles: ['user'] } });
      
      expect(guard.canActivate(context)).toBe(false);
    });

    it('should return true when user has required role', () => {
      jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin', 'user']);

      const context = createMockContext({ user: { roles: ['admin'] } });
      
      expect(guard.canActivate(context)).toBe(true);
    });

    it('should return false when user is not authenticated', () => {
      jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['user']);

      const context = createMockContext({ user: null });
      
      expect(guard.canActivate(context)).toBe(false);
    });
  });

  function createMockContext(request: any): ExecutionContext {
    return {
      switchToHttp: () => ({ getRequest: () => request }),
      getHandler: () => ({}),
      getClass: () => ({}),
      getType: () => 'http',
    } as any;
  }
});

2. Unit-тестування Interceptors

import { of } from 'rxjs';
import { LoggingInterceptor } from './logging.interceptor';

describe('LoggingInterceptor', () => {
  let interceptor: LoggingInterceptor;
  let logger: any;

  beforeEach(() => {
    logger = { log: jest.fn(), error: jest.fn() };
    interceptor = new LoggingInterceptor(logger);
  });

  it('should log request and response', (done) => {
    const context = createMockContext();
    const callHandler = { handle: () => of('result') };

    interceptor.intercept(context, callHandler).subscribe({
      next: (data) => {
        expect(data).toBe('result');
        expect(logger.log).toHaveBeenCalledTimes(2); // Before and after
        done();
      },
    });
  });

  it('should log errors', (done) => {
    const context = createMockContext();
    const error = new Error('Test error');
    const callHandler = { handle: () => throwError(() => error) };

    interceptor.intercept(context, callHandler).subscribe({
      error: (err) => {
        expect(err).toBe(error);
        expect(logger.error).toHaveBeenCalled();
        done();
      },
    });
  });

  function createMockContext(): any {
    return {
      switchToHttp: () => ({
        getRequest: () => ({ method: 'GET', url: '/test' }),
      }),
      getHandler: () => ({ name: 'testHandler' }),
      getClass: () => ({ name: 'TestController' }),
    };
  }
});

3. Unit-тестування Pipes

import { BadRequestException } from '@nestjs/common';
import { ParsePositiveIntPipe } from './parse-positive-int.pipe';

describe('ParsePositiveIntPipe', () => {
  let pipe: ParsePositiveIntPipe;

  beforeEach(() => {
    pipe = new ParsePositiveIntPipe();
  });

  it('should parse valid positive integer', () => {
    expect(pipe.transform('42', { type: 'param' })).toBe(42);
  });

  it('should throw on negative integer', () => {
    expect(() => pipe.transform('-1', { type: 'param' }))
      .toThrow(BadRequestException);
  });

  it('should throw on zero', () => {
    expect(() => pipe.transform('0', { type: 'param' }))
      .toThrow(BadRequestException);
  });

  it('should throw on non-integer', () => {
    expect(() => pipe.transform('abc', { type: 'param' }))
      .toThrow(BadRequestException);
  });

  it('should throw on float', () => {
    expect(() => pipe.transform('3.14', { type: 'param' }))
      .toThrow(BadRequestException);
  });
});

4. Unit-тестування Exception Filters

import { HttpException, HttpStatus } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';

describe('HttpExceptionFilter', () => {
  let filter: HttpExceptionFilter;

  beforeEach(() => {
    filter = new HttpExceptionFilter();
  });

  it('should format 404 error correctly', () => {
    const exception = new HttpException('Not found', HttpStatus.NOT_FOUND);
    
    const mockJson = jest.fn();
    const mockStatus = jest.fn().mockReturnValue({ json: mockJson });
    const mockResponse = { status: mockStatus };
    const mockRequest = { url: '/users/999', method: 'GET' };

    const host = {
      switchToHttp: () => ({
        getResponse: () => mockResponse,
        getRequest: () => mockRequest,
      }),
    } as any;

    filter.catch(exception, host);

    expect(mockStatus).toHaveBeenCalledWith(404);
    expect(mockJson).toHaveBeenCalledWith(
      expect.objectContaining({
        statusCode: 404,
        message: 'Not found',
      })
    );
  });
});

5. Integration-тестування Pipeline

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard';

describe('Users Pipeline (Integration)', () => {
  let app: INestApplication;
  let usersService: UsersService;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [
        UsersService,
        JwtAuthGuard,
        RolesGuard,
      ],
    }).compile();

    app = moduleFixture.createNestApplication();
    usersService = moduleFixture.get(UsersService);
    
    await app.init();
  });

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

  it('should pass through all guards and interceptors', async () => {
    const spy = jest.spyOn(usersService, 'findAll');
    
    // Імітація запиту з правильним токеном та роллю
    const request = {
      user: { id: 1, roles: ['admin'] },
    };

    // Test execution...
    
    expect(spy).toHaveBeenCalled();
  });
});

6. E2E-тестування повного Pipeline

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('Complete Pipeline (e2e)', () => {
  let app: INestApplication;
  let authToken: string;

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

    app = moduleFixture.createNestApplication();
    await app.init();

    // Отримання токену
    const loginResponse = await request(app.getHttpServer())
      .post('/auth/login')
      .send({ email: 'admin@example.com', password: 'password' });

    authToken = loginResponse.body.token;
  });

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

  describe('Full Pipeline Flow', () => {
    it('should execute complete pipeline successfully', async () => {
      return request(app.getHttpServer())
        .get('/users')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200)
        .expect(res => {
          // Перевірка що response трансформовано через TransformInterceptor
          expect(res.body).toHaveProperty('data');
          expect(res.body).toHaveProperty('timestamp');
        });
    });

    it('should reject unauthorized requests', async () => {
      return request(app.getHttpServer())
        .get('/users')
        .expect(401);
    });

    it('should validate request body', async () => {
      return request(app.getHttpServer())
        .post('/users')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ email: 'invalid-email' }) // Невалідний email
        .expect(400)
        .expect(res => {
          expect(res.body.message).toContain('Validation');
        });
    });

    it('should enforce rate limits', async () => {
      const endpoint = '/api/limited';

      // Зробити багато запитів
      for (let i = 0; i < 10; i++) {
        await request(app.getHttpServer()).get(endpoint);
      }

      // Наступний має бути заблокований
      return request(app.getHttpServer())
        .get(endpoint)
        .expect(429);
    });
  });
});

Архітектурні Patterns

1. Multi-Tenant Architecture

// interceptors/tenant.interceptor.ts
@Injectable()
export class TenantInterceptor implements NestInterceptor {
  constructor(private tenantService: TenantService) {}

  async intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.headers['x-tenant-id'];

    if (!tenantId) {
      throw new BadRequestException('Tenant ID required');
    }

    // Завантаження tenant
    const tenant = await this.tenantService.findById(tenantId);
    if (!tenant) {
      throw new NotFoundException('Tenant not found');
    }

    // Встановлення tenant у request
    request.tenant = tenant;

    // Встановлення tenant для database connection
    await this.tenantService.setCurrentTenant(tenant);

    return next.handle();
  }
}

// Використання
@Controller('api')
@UseInterceptors(TenantInterceptor)
export class ApiController {
  @Get('data')
  getData(@Req() req: Request) {
    const tenant = req['tenant'];
    // Всі DB queries автоматично фільтруються по tenant
    return this.dataService.findAll();
  }
}

2. Request Context Pattern

// request-context.service.ts
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
  private context = new Map<string, any>();

  set(key: string, value: any): void {
    this.context.set(key, value);
  }

  get<T>(key: string): T | undefined {
    return this.context.get(key);
  }
}

// interceptors/context.interceptor.ts
@Injectable()
export class ContextInterceptor implements NestInterceptor {
  constructor(private contextService: RequestContextService) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    
    // Зберігання контексту
    this.contextService.set('user', request.user);
    this.contextService.set('requestId', request.headers['x-request-id']);
    this.contextService.set('tenant', request.tenant);
    
    return next.handle();
  }
}

// Використання у сервісах
@Injectable()
export class AuditService {
  constructor(private contextService: RequestContextService) {}

  async logAction(action: string) {
    const user = this.contextService.get('user');
    const requestId = this.contextService.get('requestId');
    
    await this.auditRepository.save({
      action,
      userId: user?.id,
      requestId,
      timestamp: new Date(),
    });
  }
}

3. Feature Flags Pattern

// decorators/feature-flag.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const FEATURE_FLAG_KEY = 'featureFlag';
export const FeatureFlag = (flagName: string) => SetMetadata(FEATURE_FLAG_KEY, flagName);

// guards/feature-flag.guard.ts
@Injectable()
export class FeatureFlagGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private featureFlagsService: FeatureFlagsService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const flagName = this.reflector.get<string>(FEATURE_FLAG_KEY, context.getHandler());

    if (!flagName) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const isEnabled = await this.featureFlagsService.isEnabled(flagName, {
      userId: request.user?.id,
      tenant: request.tenant?.id,
    });

    if (!isEnabled) {
      throw new NotFoundException('Feature not available');
    }

    return true;
  }
}

// Використання
@Controller('features')
@UseGuards(FeatureFlagGuard)
export class FeaturesController {
  @Get('new-dashboard')
  @FeatureFlag('new-dashboard')
  getNewDashboard() {
    return { message: 'New dashboard' };
  }
}

4. API Versioning Pattern

// interceptors/api-version.interceptor.ts
@Injectable()
export class ApiVersionInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const version = request.headers['api-version'] || '1.0';

    request.apiVersion = version;

    return next.handle().pipe(
      map(data => {
        // Трансформація відповіді залежно від версії API
        if (version === '2.0') {
          return this.transformToV2(data);
        }
        return data;
      }),
    );
  }

  private transformToV2(data: any): any {
    // Трансформація структури для v2
    return {
      version: '2.0',
      result: data,
      metadata: {
        timestamp: new Date().toISOString(),
      },
    };
  }
}

Observability: моніторинг та debugging

1. Structured Logging

@Injectable()
export class StructuredLoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(StructuredLoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const startTime = Date.now();

    // Log request
    this.logger.log('Incoming request', {
      method: request.method,
      url: request.url,
      params: request.params,
      query: request.query,
      userId: request.user?.id,
      requestId: request.headers['x-request-id'],
      userAgent: request.headers['user-agent'],
      ip: request.ip,
    });

    return next.handle().pipe(
      tap(data => {
        const duration = Date.now() - startTime;
        
        // Log response
        this.logger.log('Request completed', {
          method: request.method,
          url: request.url,
          statusCode: 200,
          duration,
          requestId: request.headers['x-request-id'],
        });
      }),
      catchError(err => {
        const duration = Date.now() - startTime;
        
        // Log error
        this.logger.error('Request failed', {
          method: request.method,
          url: request.url,
          error: err.message,
          stack: err.stack,
          duration,
          requestId: request.headers['x-request-id'],
        });
        
        throw err;
      }),
    );
  }
}

2. Distributed Tracing

import * as opentelemetry from '@opentelemetry/api';

@Injectable()
export class TracingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const tracer = opentelemetry.trace.getTracer('nestjs-app');
    const request = context.switchToHttp().getRequest();
    
    return tracer.startActiveSpan(
      `${request.method} ${request.url}`,
      (span) => {
        span.setAttributes({
          'http.method': request.method,
          'http.url': request.url,
          'http.user_agent': request.headers['user-agent'],
          'user.id': request.user?.id,
        });

        return next.handle().pipe(
          tap(() => {
            span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
            span.end();
          }),
          catchError(err => {
            span.setStatus({
              code: opentelemetry.SpanStatusCode.ERROR,
              message: err.message,
            });
            span.recordException(err);
            span.end();
            throw err;
          }),
        );
      },
    );
  }
}

3. Metrics Collection

@Injectable()
export class MetricsInterceptor implements NestInterceptor {
  constructor(private metricsService: MetricsService) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const startTime = Date.now();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - startTime;
        
        // Збір метрик
        this.metricsService.recordHttpRequest({
          method: request.method,
          endpoint: request.route?.path || request.url,
          statusCode: 200,
          duration,
        });
      }),
      catchError(err => {
        const duration = Date.now() - startTime;
        
        this.metricsService.recordHttpRequest({
          method: request.method,
          endpoint: request.route?.path || request.url,
          statusCode: err.status || 500,
          duration,
        });
        
        throw err;
      }),
    );
  }
}

Checklist: контрольний список для Pipeline

Перед впровадженням компонента

  • Single Responsibility: компонент виконує одну чітко визначену задачу
  • Performance: мінімізовано async операції та DB queries
  • Error Handling: правильна обробка виключень з інформативними повідомленнями
  • Testing: написано unit-тести (70%+ coverage)
  • Observability: додано логування для дебагу
  • Documentation: додано JSDoc коментарі та приклади використання

Для Guards

  • Не виконує бізнес-логіку (лише перевірка доступу)
  • Використовує Reflector для метаданих
  • Надає можливість пропуску через metadata декоратор
  • Викидає зрозумілі виключення (UnauthorizedException, ForbiddenException)
  • Async операції мінімізовані

Для Interceptors

  • Не модифікує request напряму (immutable підхід)
  • Правильно обробляє помилки у pipe operators
  • Використовує tap() для side effects
  • Не блокує response (async операції у setImmediate або окремих потоках)
  • Має можливість умовного виконання

Для Pipes

  • Викидає BadRequestException при невалідних даних
  • Має чіткий error message
  • Підтримує трансформацію типів
  • Не має side effects (чистий pipe)
  • Швидко виконується (без DB queries)

Для Exception Filters

  • Не розкриває стек трейси у production
  • Логує помилки з контекстом
  • Відправляє 5xx помилки у Sentry/Datadog
  • Повертає структуровані JSON-відповіді
  • Приховує чутливу інформацію

Діаграма: повний Request Pipeline flow

graph TD
    A[Client Request] --> B[Middleware Layer]
    B --> C{Global Guards}
    C -->|Unauthorized| Z1[401 Response]
    C -->|Authorized| D{Controller Guards}
    D -->|Forbidden| Z2[403 Response]
    D -->|Authorized| E[Global Interceptors Before]
    E --> F[Controller Interceptors Before]
    F --> G[Route Interceptors Before]
    G --> H[Global Pipes]
    H --> I[Controller Pipes]
    I --> J[Route Pipes]
    J --> K[Parameter Pipes]
    K -->|Validation Failed| Z3[400 Response]
    K -->|Valid| L[Handler Execution]
    
    L -->|Success| M[Route Interceptors After]
    L -->|Exception| EF[Exception Filters]
    
    M --> N[Controller Interceptors After]
    N --> O[Global Interceptors After]
    O --> P[Response Transformation]
    P --> Q[Client Response]
    
    EF --> R{Filter Match?}
    R -->|Yes| S[Format Error]
    R -->|No| T[Next Filter]
    T --> R
    S --> U[Error Response]
    U --> Q
    
    style C fill:#ffd93d
    style D fill:#ffd93d
    style H fill:#6bcf7f
    style I fill:#6bcf7f
    style J fill:#6bcf7f
    style K fill:#6bcf7f
    style E fill:#a8dadc
    style F fill:#a8dadc
    style G fill:#a8dadc
    style M fill:#a8dadc
    style N fill:#a8dadc
    style O fill:#a8dadc
    style EF fill:#ff6b6b

Легенда:

  • Жовтий (Guards): перевірка аутентифікації та авторизації
  • Зелений (Pipes): валідація та трансформація даних
  • Блакитний (Interceptors): логування, трансформація, кешування
  • Червоний (Exception Filters): обробка помилок

Підсумок

🎯 Single Responsibility

Кожен компонент Pipeline виконує одну задачу. Розділяйте аутентифікацію, авторизацію, rate limiting на окремі Guards. Спрощує тестування та підтримку.

Правило: один Guard = одна перевірка

⚡ Performance Optimization

Мінімізуйте async операції через Promise.all(). Кешуйте метадані Reflector. Використовуйте lazy loading для важких сервісів. Уникайте блокування response.

Ключ: batch queries, кешування, умовне виконання

🛡️ Error Handling Strategy

Централізована обробка через Exception Filters. Ієрархія кастомних виключень з кодами помилок. Circuit Breaker для зовнішніх API. Логування з контекстом.

Підхід: catch all → log → transform → respond

🧪 Testing Pyramid

70% unit-тести (окремі компоненти), 20% integration (взаємодія), 10% E2E (повні сценарії). Мокування ExecutionContext та Reflector. Supertest для E2E.

Структура: unit → integration → e2e

🏗️ Architectural Patterns

Multi-tenant через TenantInterceptor. Request Context для зберігання даних запиту. Feature Flags для A/B тестування. API Versioning для backward compatibility.

Patterns: tenant isolation, feature toggles, versioning

📊 Observability

Structured logging з контекстом (requestId, userId, duration). Distributed tracing через OpenTelemetry. Metrics collection (request count, duration, errors).

Інструменти: Winston, OpenTelemetry, Prometheus

✅ Quality Checklist

Перевірка перед deploy: SRP дотримано, performance оптимізовано, error handling налаштовано, тести написано, логування додано, документація оновлена.

Мінімум: 70% coverage, structured logs, error tracking

🔄 Pipeline Flow

Middleware → Guards → Interceptors Before → Pipes → Handler → Interceptors After → Filters. Глобальні виконуються перед локальними. Порядок реєстрації = порядок виконання.

Запам'ятати: global → controller → route → parameter

Часті запитання (FAQ)


Вітаємо! Ви завершили вивчення NestJS Validation Pipeline. Тепер ви володієте знаннями про:

  • Middleware, Guards, Interceptors, Pipes, Exception Filters
  • Execution Context, ArgumentsHost, Reflector
  • Custom Decorators (parameter та metadata)
  • Global Components та APP_* токени
  • Best Practices: SRP, performance, error handling, testing

Наступні кроки:

  • Практикуйте створення власних компонентів Pipeline
  • Інтегруйте з реальними проектами
  • Вивчайте advanced patterns (multi-tenant, microservices)
  • Розширюйте знання про NestJS ecosystem (TypeORM, GraphQL, WebSockets)

Успіхів у розробці! 🚀

Copyright © 2026