Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Entwicklung · SKILL.md

nestjs-patterns

nestjs-patterns

Use this skill when implementing NestJS patterns — CQRS with @nestjs/cqrs, guards/interceptors/pipes/filters, event sourcing, custom decorators, microservices, and provider patterns. This skill enforces: ExecutionContext-based guards, RxJS in interceptors, decorator composition, typed event payloads. Requires @nestjs/core. Do NOT use for: Module structure, …

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

name: nestjs-patterns description: > Use this skill when implementing NestJS patterns — CQRS with @nestjs/cqrs, guards/interceptors/pipes/filters, event sourcing, custom decorators, microservices, and provider patterns. This skill enforces: ExecutionContext-based guards, RxJS in interceptors, decorator composition, typed event payloads. Requires @nestjs/core. Do NOT use for: Module structure, project setup, or non-NestJS applications. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, nestjs, phase-6]

NestJS Patterns

Purpose

Implement NestJS-specific patterns — CQRS, guards/interceptors, event sourcing, custom decorators, microservices transport, and advanced provider patterns.

Agent Protocol

Trigger

User request includes: NestJS guard, NestJS interceptor, NestJS pipe, NestJS filter, NestJS CQRS, NestJS event sourcing, NestJS microservice, NestJS decorator, NestJS provider.

Input Context

  • Pattern needed (Guard, Interceptor, Pipe, Filter, CQRS, Microservice)
  • Transport type for microservices (TCP, Redis, RabbitMQ, Kafka)
  • Module structure and DDD approach

Output Artifact

Code examples for the requested patterns — guard implementation, interceptor chain, CQRS handler, etc.

Response Format

Code-first output. Pattern name, problem, types, implementation, usage. No preamble.

Completion Criteria

  • Guard uses ExecutionContext and returns boolean
  • Interceptor uses Observable and RxJS operators
  • Pipe transforms value or throws
  • Filter catches specific exception types
  • CQRS with command/query separation
  • Microservice with transport strategy

Max Response Length

4096 tokens

Architecture Decision Trees

Guard vs Interceptor vs Middleware

AspectGuardInterceptorMiddleware
DecisionAllow/deny requestTransform response/requestPre-processing
Execution contextFull (route, args)Full (call handler)Limited (request/response)
RxJS accessNoYes (Observable)No
DI scopeModule providersModule providersGlobally registered
Use caseAuth, roles, permissionsCaching, logging, mappingCORS, session, compression

Decision: Gate (allow/deny) → Guard. Transform (in/out) → Interceptor. Pre-route (no Nest context) → Middleware.

Transport Strategy for Microservices

TransportProsConsBest For
TCPSimple, no brokerPoint-to-point onlyInternal microservices
RedisPub/sub, fastRequires RedisBroadcast, real-time
RabbitMQReliable, DLQ, routingHeavier opsComplex workflows, async
KafkaHigh throughput, replayComplex setupEvent streaming, audit logs
gRPCStrong contracts, perfLimited ecosystemCross-language

Decision: Simple internal → TCP. Pub/sub + real-time → Redis. Reliable async → RabbitMQ. High-volume streaming → Kafka. Cross-language contracts → gRPC.

Workflow

Step 1: Advanced Guards

// guards/roles.guard.ts
import { SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

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

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

// guards/throttle.guard.ts
@Injectable()
export class ThrottleGuard implements CanActivate {
  private readonly requests = new Map<string, number[]>();

  constructor(
    @Inject('THROTTLE_CONFIG')
    private readonly config: { limit: number; ttl: number },
  ) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const key = request.ip;
    const now = Date.now();
    const timestamps = this.requests.get(key) || [];

    const recent = timestamps.filter(t => now - t < this.config.ttl);
    recent.push(now);
    this.requests.set(key, recent);

    if (recent.length > this.config.limit) {
      throw new HttpException('Too many requests', 429);
    }
    return true;
  }
}

// Usage
@Controller('users')
@UseGuards(RolesGuard)
export class UserController {
  @Post()
  @Roles('admin')
  async create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

Step 2: Advanced Interceptors

// interceptors/cache.interceptor.ts
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  private readonly cache = new Map<string, { data: any; expiry: number }>();

  constructor(@Inject('CACHE_TTL') private readonly ttl: number) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const key = `${request.method}:${request.url}`;
    const cached = this.cache.get(key);

    if (cached && cached.expiry > Date.now()) {
      return of(cached.data);
    }

    return next.handle().pipe(
      tap(data => {
        this.cache.set(key, { data, expiry: Date.now() + this.ttl });
      }),
    );
  }
}

// interceptors/timeout.interceptor.ts
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  constructor(@Inject('TIMEOUT') private readonly timeout: number) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(this.timeout),
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(() => new HttpException('Request timeout', 408));
        }
        return throwError(() => err);
      }),
    );
  }
}

// interceptors/audit.interceptor.ts
@Injectable()
export class AuditInterceptor implements NestInterceptor {
  constructor(
    private readonly auditService: AuditService,
    private readonly logger: Logger,
  ) {}

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

    return next.handle().pipe(
      finalize(() => {
        const duration = Date.now() - startTime;
        this.auditService.log({
          method, url, userId: user?.id, ip, duration,
          statusCode: context.switchToHttp().getResponse().statusCode,
          timestamp: new Date(),
        }).catch(e => this.logger.error('Audit log failed', e));
      }),
    );
  }
}

Step 3: Custom Pipes

// pipes/parse-optional-uuid.pipe.ts
@Injectable()
export class ParseOptionalUUIDPipe implements PipeTransform<string, UUID | undefined> {
  transform(value: string): UUID | undefined {
    if (!value) return undefined;
    if (!isUUID(value)) {
      throw new BadRequestException(`Invalid UUID: ${value}`);
    }
    return value as UUID;
  }
}

// pipes/validation.pipe.ts
@Injectable()
export class CustomValidationPipe implements PipeTransform {
  constructor(private readonly schema: ZodSchema) {}

  transform(value: unknown): unknown {
    const result = this.schema.safeParse(value);
    if (!result.success) {
      throw new BadRequestException({
        code: 'VALIDATION_ERROR',
        details: result.error.issues.map(i => ({
          path: i.path.join('.'),
          message: i.message,
        })),
      });
    }
    return result.data;
  }
}

Step 4: Exception Filters

// filters/http-exception.filter.ts
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

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

    let status = 500;
    let errorResponse: ErrorResponse = {
      success: false,
      error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' },
    };

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const exResponse = exception.getResponse();
      errorResponse = {
        success: false,
        error: {
          code: HttpStatus[status] || 'ERROR',
          message: typeof exResponse === 'string' ? exResponse : (exResponse as any).message,
        },
      };
    } else if (exception instanceof ZodValidationError) {
      status = 400;
      errorResponse = {
        success: false,
        error: {
          code: 'VALIDATION_ERROR',
          details: exception.errors,
        },
      };
    }

    this.logger.error(`${request.method} ${request.url} ${status}`, exception instanceof Error ? exception.stack : '');

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

Step 5: CQRS with Sagas

import { Saga, ICommand, ofType } from '@nestjs/cqrs';

@Injectable()
export class OrdersSaga {
  @Saga()
  orderCreated = (events$: Observable<any>): Observable<ICommand> => {
    return events$.pipe(
      ofType(OrderCreatedEvent),
      delay(1000), // wait for consistency
      map(event => new CheckInventoryCommand(event.orderId)),
    );
  };

  @Saga()
  inventoryChecked = (events$: Observable<any>): Observable<ICommand> => {
    return events$.pipe(
      ofType(InventoryCheckedEvent),
      mergeMap(event =>
        event.inStock
          ? of(new ProcessPaymentCommand(event.orderId))
          : of(new CancelOrderCommand(event.orderId, 'Out of stock')),
      ),
    );
  };
}

Step 6: Microservice Pattern

// apps/api/src/main.ts — hybrid app (HTTP + microservice)
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Hybrid: HTTP + TCP microservice
  app.connectMicroservice({
    transport: Transport.TCP,
    options: { port: 3001 },
  });

  // Also connect RabbitMQ for async events
  app.connectMicroservice({
    transport: Transport.RMQ,
    options: {
      urls: ['amqp://localhost:5672'],
      queue: 'orders_queue',
      queueOptions: { durable: true },
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
}

// apps/worker/src/worker.module.ts — standalone consumer
@Module({
  imports: [SharedModule, DatabaseModule],
  controllers: [OrdersController],  // Microservice controllers
})
export class WorkerModule {}

// apps/worker/src/orders.controller.ts
@Controller()
export class OrdersController {
  constructor(private readonly orderService: OrderService) {}

  @MessagePattern({ cmd: 'create_order' })
  async createOrder(@Payload() dto: CreateOrderDto): Promise<Order> {
    return this.orderService.create(dto);
  }

  @MessagePattern({ cmd: 'get_order' })
  async getOrder(@Payload() data: { id: string }): Promise<Order> {
    return this.orderService.findById(data.id);
  }

  @EventPattern('order_created')
  async handleOrderCreated(@Payload() data: OrderEvent) {
    // Event handler (fire-and-forget)
    await this.inventoryService.checkStock(data.orderId);
  }
}

Implementation Patterns

Pattern: Custom Decorator Composition

import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';

export function Auth(...roles: string[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(JwtAuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' }),
  );
}

// Usage
@Controller('admin')
export class AdminController {
  @Get('users')
  @Auth('admin', 'superadmin')
  findAll() {
    return this.userService.findAll();
  }

  @Post('users')
  @Auth('superadmin')
  create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

Pattern: Provider Factory

// Dynamic value provider
const cacheProvider = {
  provide: 'CACHE',
  useFactory: (config: ConfigService) => {
    return new CacheManager({
      ttl: config.get('cache.ttl'),
      max: config.get('cache.maxItems'),
    });
  },
  inject: [ConfigService],
};

// Async provider
const databaseProvider = {
  provide: 'DATABASE_CONNECTION',
  useFactory: async (config: ConfigService) => {
    const connection = await createConnection(config.get('database'));
    return connection;
  },
  inject: [ConfigService],
};

Production Considerations

Graceful Shutdown with Microservices

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  // ... setup microservices
  const signals = ['SIGTERM', 'SIGINT'];
  signals.forEach(signal =>
    process.on(signal, async () => {
      await app.close();
      process.exit(0);
    }),
  );
}

Distributed Tracing

Use nestjs-opentelemetry or @opentelemetry/instrumentation-nestjs-core for automatic tracing of controllers, providers, and guards.

Anti-Patterns

Anti-PatternWhyFix
Business logic in guardsNot testable across transportsDelegate to service
async interceptors without proper error propagationSwallowed errorsUse catchError RxJS operator
@Catch() with no argumentCatches everything, hides errorsCatch specific exception types
Direct @Client usage for microservicesTight coupling to transportUse @nestjs/microservices ClientProxy
Guards checking user in requestIgnores ExecutionContext genericsUse context.switchToHttp().getRequest()

Security Considerations

  • @nestjs/throttler for rate limiting at controller level
  • Guards can access ExecutionContext for fine-grained auth
  • Use @nestjs/passport with JWT strategy for auth — never implement JWT verification manually
  • RBAC via custom @Roles('admin') decorator with RolesGuard
  • Serialization interceptor to @Exclude() sensitive fields (passwords, tokens)
  • CORS via app.enableCors() — restrict origins in production

Testing Strategies

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

  beforeEach(() => {
    reflector = new Reflector();
    guard = new RolesGuard(reflector);
  });

  it('should allow access when roles match', () => {
    const context = createMockExecutionContext({
      user: { roles: ['admin'] },
    });
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
    expect(guard.canActivate(context)).toBe(true);
  });

  it('should deny access when roles do not match', () => {
    const context = createMockExecutionContext({
      user: { roles: ['user'] },
    });
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
    expect(guard.canActivate(context)).toBe(false);
  });
});

Test interceptors by mocking ExecutionContext and CallHandler. Test pipes with direct invocation and expected transforms.

Rules

  • Guards return boolean or throw. Never modify request/response.
  • Interceptors transform using RxJS. Return Observable<T>.
  • Pipes transform value or throw BadRequestException.
  • Filters catch HttpException subclasses. Last resort for unhandled.
  • CQRS: Commands mutate, Queries return data. Events trigger side effects.
  • Microservice @MessagePattern expects reply, @EventPattern is fire-and-forget.
  • @nestjs/testing Test.createTestingModule for all unit tests.

References

  • references/cqrs-events.md — CQRS and Events
  • references/decorators-metadata.md — Custom Decorators and Metadata
  • references/guards-interceptors.md — Guards and Interceptors
  • references/microservices.md — Microservices with NestJS
  • references/nestjs-guards-interceptors.md — Guards/Interceptors Deep Dive
  • references/nestjs-providers-patterns.md — Provider Patterns
  • references/nestjs-testing.md — Testing NestJS
  • references/pipes-filters.md — Pipes and Filters

Handoff

Hand off to backend/nestjs/architecture/SKILL.md for module structure or backend/universal/api-response/SKILL.md for API response formatting.

Gespeicherte Skills