La interfaz está localizada; las descripciones y documentos de las Skills se traducen automáticamente con IA a su idioma. El código, los comandos, rutas y términos técnicos se mantienen en el original, y prevalece el documento del autor.
Explorar Skills
Diseño y creatividad · SKILL.md

nestjs-architecture

nestjs-architecture

Use this skill when structuring NestJS applications — module-based architecture, dependency injection, provider scopes, dynamic modules, and monorepo structure. This skill enforces: one module per domain, provider encapsulation, module boundary enforcement, global vs scoped providers. Requires @nestjs/core. Do NOT use for: Express-only projects, React/Vue f…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentación de la Skill

GitHub

name: nestjs-architecture description: > Use this skill when structuring NestJS applications — module-based architecture, dependency injection, provider scopes, dynamic modules, and monorepo structure. This skill enforces: one module per domain, provider encapsulation, module boundary enforcement, global vs scoped providers. Requires @nestjs/core. Do NOT use for: Express-only projects, React/Vue frontend, or non-NestJS Node.js frameworks. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, nestjs, phase-6]

NestJS Architecture

Purpose

Structure NestJS applications with module-based architecture — domain modules, shared modules, dynamic modules, monorepo projects, and provider scoping.

Agent Protocol

Trigger

User request includes: NestJS module structure, NestJS architecture, NestJS project layout, NestJS DI, NestJS modules, NestJS monorepo, NestJS providers.

Input Context

  • NestJS version (10+)
  • Architecture style (modular, monorepo, microservices)
  • Database (TypeORM, Prisma, MikroORM, Mongoose)
  • Transport (HTTP, WebSocket, TCP, RabbitMQ)

Output Artifact

Module structure, provider registration, module exports, monorepo layout, DI setup.

Response Format

Produce artifact directly. No preamble, no postamble, no explanations.

Completion Criteria

  • Root module imports feature modules
  • Each domain has its own module with controllers, services, repositories
  • Modules export only what other modules need
  • Global modules registered sparingly (shared infra only)
  • Dynamic modules configured with forRoot/forFeature

Max Response Length

4096 tokens

Architecture Decision Trees

Module Organization: Feature vs Shared vs Core

Module TypeContainsExport StrategyExample
FeatureControllers, Services, RepositoriesOnly what others needUsersModule, OrdersModule
SharedReusable utilities, guards, filtersEverythingCommonModule
CoreSingletons (DB, config, logger)GlobalDatabaseModule, ConfigModule

Decision: Domain logic → Feature Module. Cross-cutting concern → Shared Module. Infrastructure singleton → Global Core Module.

Provider Scope Decision

ScopeLifetimeUse Case
DEFAULT (singleton)One per appServices, repositories, DB connections
TRANSIENTNew per injectionStateful validators, per-request cache
REQUESTNew per HTTP requestRequest-scoped data (tenant, user)

Decision: Stateless service → DEFAULT. Stateful per-instance → TRANSIENT. Per-request data → REQUEST.

Workflow

Step 1: Project Structure

src/
  main.ts                              # Bootstrap
  app.module.ts                        # Root module
  app.controller.ts                    # Health check
  common/
    decorators/
      current-user.decorator.ts
      roles.decorator.ts
    guards/
      auth.guard.ts
      roles.guard.ts
    filters/
      http-exception.filter.ts
    interceptors/
      logging.interceptor.ts
      transform.interceptor.ts
    pipes/
      validation.pipe.ts
    interfaces/
      user-request.interface.ts
  modules/
    users/
      user.module.ts
      user.controller.ts
      user.service.ts
      user.repository.ts
      user.entity.ts
      dto/
        create-user.dto.ts
        update-user.dto.ts
    orders/
      order.module.ts
      order.controller.ts
      order.service.ts
      order.entity.ts
      dto/
  config/
    database.config.ts
    app.config.ts

Step 2: Feature Module

// modules/users/user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService, UserRepository],
  exports: [UserService],
})
export class UsersModule {}

// modules/users/user.controller.ts
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  async findAll(@Query() pagination: PaginationDto) {
    return this.userService.findAll(pagination);
  }

  @Get(':id')
  async findOne(@Param('id', ParseUUIDPipe) id: string) {
    return this.userService.findById(id);
  }

  @Post()
  @UsePipes(new ValidationPipe({ whitelist: true }))
  async create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }

  @Patch(':id')
  async update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateUserDto) {
    return this.userService.update(id, dto);
  }

  @Delete(':id')
  @HttpCode(204)
  async remove(@Param('id', ParseUUIDPipe) id: string) {
    return this.userService.delete(id);
  }
}

// modules/users/user.service.ts
@Injectable()
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  async create(dto: CreateUserDto): Promise<User> {
    const existing = await this.userRepository.findByEmail(dto.email);
    if (existing) {
      throw new ConflictException('Email already exists');
    }
    const user = await this.userRepository.save(dto.toEntity());
    this.eventEmitter.emit('user.created', user);
    return user;
  }
}

Step 3: Common Module (Shared)

// common/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return data ? user?.[data] : user;
  },
);

// common/guards/auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);
    if (!token) throw new UnauthorizedException();
    try {
      const payload = this.jwtService.verify(token);
      request.user = payload;
      return true;
    } catch {
      throw new UnauthorizedException();
    }
  }

  private extractToken(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

// common/interceptors/transform.interceptor.ts
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

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

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

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

Step 4: Dynamic Module Pattern

// config/database.config.ts
export interface DatabaseModuleOptions {
  type: 'postgres' | 'mysql' | 'sqlite';
  host: string;
  port: number;
  username: string;
  password: string;
  database: string;
}

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseModuleOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      imports: [
        TypeOrmModule.forRoot({
          type: options.type,
          host: options.host,
          port: options.port,
          username: options.username,
          password: options.password,
          database: options.database,
          autoLoadEntities: true,
          synchronize: false,
          logging: process.env.NODE_ENV === 'development',
        }),
      ],
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: options,
        },
      ],
      exports: ['DATABASE_OPTIONS'],
    };
  }

  static forFeature(models: EntityClassOrSchema[]): DynamicModule {
    return {
      module: DatabaseModule,
      imports: [TypeOrmModule.forFeature(models)],
      exports: [TypeOrmModule],
    };
  }
}

// Usage in app.module.ts
@Module({
  imports: [
    DatabaseModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT),
      username: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME,
    }),
    UsersModule,
    OrdersModule,
    CommonModule,
  ],
  controllers: [AppController],
  providers: [
    { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
    { provide: APP_FILTER, useClass: HttpExceptionFilter },
  ],
})
export class AppModule {}

Step 5: Monorepo Structure

apps/
  api/
    src/
      main.ts
      app.module.ts
  admin/
    src/
      main.ts
      admin.module.ts
  worker/
    src/
      main.ts
      worker.module.ts
libs/
  common/
    src/
      decorators/
      guards/
      interceptors/
      filters/
      common.module.ts
  database/
    src/
      database.module.ts
      entities/
      repositories/
shared/
  types/
  interfaces/
nest-cli.json
// nest-cli.json (monorepo)
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "monorepo": true,
  "root": "apps/api",
  "projects": {
    "api": { "type": "application", "root": "apps/api", "entryFile": "main" },
    "admin": { "type": "application", "root": "apps/admin", "entryFile": "main" },
    "worker": { "type": "application", "root": "apps/worker", "entryFile": "main" },
    "common": { "type": "library", "root": "libs/common" },
    "database": { "type": "library", "root": "libs/database" }
  }
}

Step 6: Config Module

// config/app.config.ts
export default registerAs('app', () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  corsOrigin: process.env.CORS_ORIGIN || '*',
  jwtSecret: process.env.JWT_SECRET,
  jwtExpiresIn: process.env.JWT_EXPIRES_IN || '1d',
  database: {
    url: process.env.DATABASE_URL,
    ssl: process.env.NODE_ENV === 'production',
  },
}));

// Usage in service
@Injectable()
export class AppService {
  constructor(
    @InjectConfig() private readonly appConfig: ConfigType<typeof appConfig>,
  ) {}

  getPort(): number {
    return this.appConfig.port;
  }
}

Implementation Patterns

Pattern: CQRS with @nestjs/cqrs

// Create command
export class CreateUserCommand {
  constructor(public readonly dto: CreateUserDto) {}
}

// Command handler
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
  constructor(private readonly userRepository: UserRepository) {}

  async execute(command: CreateUserCommand): Promise<User> {
    const { dto } = command;
    return this.userRepository.save(dto.toEntity());
  }
}

// Controller dispatches
@Post()
async create(@Body() dto: CreateUserDto) {
  return this.commandBus.execute(new CreateUserCommand(dto));
}

Pattern: Events and Event Handlers

// Event
export class UserCreatedEvent {
  constructor(public readonly user: User) {}
}

// Event handler
@EventsHandler(UserCreatedEvent)
export class UserCreatedHandler implements IEventHandler<UserCreatedEvent> {
  constructor(
    private readonly emailService: EmailService,
    private readonly analyticsService: AnalyticsService,
  ) {}

  async handle(event: UserCreatedEvent) {
    await Promise.all([
      this.emailService.sendWelcome(event.user),
      this.analyticsService.track('user.created', event.user.id),
    ]);
  }
}

Production Considerations

Graceful Shutdown

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  app.enableCors({ origin: process.env.CORS_ORIGIN });
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
  app.useGlobalInterceptors(new TransformInterceptor());

  const port = process.env.PORT || 3000;
  await app.listen(port);
  console.log(`Application running on port ${port}`);
}

OpenTelemetry

// Register OpenTelemetry SDK
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const otelSDK = new NodeSDK({
  instrumentations: [getNodeAutoInstrumentations()],
});
otelSDK.start();

Anti-Patterns

Anti-PatternWhyFix
Circular module importsDI fails at runtimeUse shared module or forwardRef
All providers in root moduleNo encapsulation, poor reusabilityPer-feature modules with exports
Global guards registered inlineCan't test without full moduleUse APP_GUARD token in providers
Services with multiple repo injectionsSRP violationSplit into smaller services
Directly importing TypeORM module in featureTight coupling to ORMUse repository pattern via DI

Security Considerations

  • Guards for auth, not middleware (Guards have ExecutionContext context)
  • ValidationPipe with whitelist: true to strip unknown properties
  • ParseUUIDPipe on all ID params to prevent injection
  • Helmet: app.use(helmet()) at bootstrap
  • Rate limiting: @nestjs/throttler module
  • CSRF: use csurf middleware for cookie-based auth
  • Serialization: @Exclude() / @ClassSerializerInterceptor to hide sensitive fields

Testing Strategies

describe('UserService', () => {
  let service: UserService;
  let repository: MockType<UserRepository>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UserService,
        { provide: UserRepository, useFactory: repositoryMockFactory },
        { provide: EventEmitter2, useValue: { emit: jest.fn() } },
      ],
    }).compile();

    service = module.get(UserService);
    repository = module.get(UserRepository);
  });

  it('should throw on duplicate email', async () => {
    repository.findByEmail.mockResolvedValue({ email: 'test@test.com' });
    await expect(service.create({ email: 'test@test.com', name: 'Test' }))
      .rejects.toThrow(ConflictException);
  });
});

Use @nestjs/testing for unit tests. Use supertest with createNestApplication for e2e tests. Use TestContainers for database integration tests.

Rules

  • One module per domain/bounded context. No mega-modules.
  • Modules export only what is needed by other modules — keep the public API small.
  • Feature modules import TypeOrmModule.forFeature, never directly import database connection.
  • APP_* tokens (APP_GUARD, APP_INTERCEPTOR, APP_FILTER, APP_PIPE) in root module only.
  • All injections via constructor — no @Inject() on properties.
  • Services are @Injectable(), controllers are @Controller(), both in module providers/controllers.
  • Pipes for validation, Guards for authorization, Interceptors for transformation, Filters for error handling.

References

  • references/ddd-nestjs.md — DDD with NestJS
  • references/module-structure.md — Module Structure Guidelines
  • references/nestjs-cqrs.md — CQRS with NestJS
  • references/nestjs-deployment.md — NestJS Deployment
  • references/nestjs-modules.md — NestJS Modules Deep Dive
  • references/nestjs-testing.md — NestJS Testing

Handoff

Hand off to backend/nestjs/patterns/SKILL.md for NestJS-specific patterns or backend/universal/api-response/SKILL.md for API response formatting.

Skills guardadas