本文へ移動
cccskills
無料GitHub で公開

nest-conventions

NestJS module structure, dependency injection, lifecycle, configuration, exception handling, and logging conventions. Apply when implementing or modifying NestJS backend code. Use this skill to: - Structure feature modules (one feature = one module). - Wire DI correctly (constructor injection, scopes, custom tokens). - Set up ConfigModule, ValidationPipe, exception filters in main.ts. - Use lifecycle hooks for init and graceful shutdown. - Pick the right exception class for each error case. Do NOT use this skill for: - Decorator-specific patterns (see decorator-patterns). - ORM/data access (see nest-data-layer). - GraphQL/WebSockets/Microservices (see nest-advanced). - Testing (see nest-testing).

インストール方法を見る

含まれるファイル(1)

  • SKILL.md11.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

NestJS Conventions

This skill encodes module-level idioms for NestJS backend code. Apply alongside decorator-patterns (decorator-specific syntax) and js-foundation:typescript-patterns (general TS strictness).

Project layout

Standalone application:

project-root/
├── nest-cli.json
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── src/
│   ├── main.ts                    # bootstrap
│   ├── app.module.ts              # root module
│   ├── app.controller.ts          # optional, often just /healthz
│   ├── app.service.ts             # optional
│   ├── config/                    # config files (app.config.ts, db.config.ts)
│   ├── common/                    # shared (filters, interceptors, pipes, decorators)
│   │   ├── filters/
│   │   ├── interceptors/
│   │   ├── pipes/
│   │   └── decorators/
│   ├── users/                     # feature module
│   │   ├── users.module.ts
│   │   ├── users.controller.ts
│   │   ├── users.service.ts
│   │   ├── dto/
│   │   │   ├── create-user.dto.ts
│   │   │   └── update-user.dto.ts
│   │   ├── entities/
│   │   │   └── user.entity.ts
│   │   └── tests/                 # often co-located OR in /test
│   ├── auth/
│   └── ...
├── test/                          # e2e tests
│   └── *.e2e-spec.ts
└── dist/                          # build output, gitignored

Monorepo (Nx-flavored, nest-cli.json "monorepo": true):

project-root/
├── nest-cli.json
├── apps/
│   └── api/
│       └── src/
└── libs/
    ├── shared/
    └── users/

Mirror what exists. Don't introduce a new layout pattern for one feature.

Module structure

One feature = one module:

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],            // export only what other modules need
})
export class UsersModule {}

Wire into root:

// src/app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({...}),
    UsersModule,
    AuthModule,
  ],
})
export class AppModule {}

@Global() sparingly

@Global()
@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class LoggerModule {}

Use only for genuinely cross-cutting deps (logger, config, DB connection). For two unrelated features needing the same service — explicit import is clearer.

Circular imports — refactor first

If UsersModule and OrdersModule import each other:

  1. Extract shared types to a shared/ lib.
  2. Or merge them if conceptually one bounded context.
  3. Last resort: forwardRef(() => OtherModule) with a comment explaining the dependency cycle.
imports: [forwardRef(() => OrdersModule)]
// circular: User has many Orders; Order belongs to User. Type-only import in service.

Dependency injection

Constructor injection

@Injectable()
export class UsersService {
  constructor(
    private readonly users: Repository<User>,
    private readonly mailer: MailerService,
    @Inject('AUDIT_LOG') private readonly audit: AuditLog,
  ) {}
}
  • private readonly — DI props don't get reassigned.
  • Type-based tokens for classes; string/symbol tokens for non-class values via @Inject('TOKEN').

Custom providers

@Module({
  providers: [
    UsersService,
    {
      provide: 'AUDIT_LOG',
      useClass: ProductionAuditLog,
    },
    {
      provide: 'CONFIG_OPTIONS',
      useFactory: (config: ConfigService) => ({
        retries: config.get<number>('RETRIES', 3),
      }),
      inject: [ConfigService],
    },
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
  • useClass — bind interface to implementation.
  • useValue — static value (constants, configs).
  • useFactory — runtime computation; declare inject for factory deps.
  • useExisting — alias another provider.

Scopes

@Injectable({ scope: Scope.DEFAULT })  // singleton (implicit default)
@Injectable({ scope: Scope.REQUEST })  // per-request — propagates UP the dep chain
@Injectable({ scope: Scope.TRANSIENT }) // new instance per inject

Scope.REQUEST is contagious — anything injecting a request-scoped provider becomes request-scoped too. Use only when you genuinely need per-request state (e.g., request-bound logger context). Costly otherwise.

Configuration

ConfigModule setup

import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'test', 'production').required(),
        PORT: Joi.number().port().default(3000),
        DATABASE_URL: Joi.string().uri().required(),
      }),
      validationOptions: { abortEarly: false },
    }),
  ],
})

If validationSchema fails on boot, the app crashes loud — exactly what you want.

Inject ConfigService

@Injectable()
export class DatabaseService {
  constructor(private readonly config: ConfigService) {
    const url = this.config.get<string>('DATABASE_URL', { infer: true });
  }
}
  • infer: true — TypeScript infers return type from schema if you typed it.
  • Provide default as 2nd arg only when truly optional: config.get('CACHE_TTL', 60).
  • Never process.env.X outside config/ setup files.

Per-feature config

// src/config/database.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT ?? '5432', 10),
  url: process.env.DATABASE_URL,
}));

// inject in feature
@Injectable()
class DbModuleOptions {
  constructor(@Inject(databaseConfig.KEY) private cfg: ConfigType<typeof databaseConfig>) {}
}

Validation pipe (global)

// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,                  // strip unknown fields
    forbidNonWhitelisted: true,       // reject requests with unknown fields
    transform: true,                  // class-transformer auto-cast
    transformOptions: { enableImplicitConversion: true },
  }));

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

Without whitelist + forbidNonWhitelisted, extra payload fields silently pass through — that's a real source of mass-assignment bugs.

Exception handling

Built-in HTTP exceptions

throw new BadRequestException('email already in use');
throw new UnauthorizedException();                       // 401
throw new ForbiddenException('insufficient role');       // 403
throw new NotFoundException(`user ${id} not found`);     // 404
throw new ConflictException('version mismatch');         // 409
throw new UnprocessableEntityException(...);             // 422
throw new InternalServerErrorException();                // 500 (last resort)

For non-HTTP layers (microservice handlers, WebSocket gateways), throw plain Error subclasses; map at the boundary via filters or transports' built-in error handling.

Custom exception filter

@Catch(MyDomainException)
export class MyDomainExceptionFilter implements ExceptionFilter {
  catch(exception: MyDomainException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    response.status(exception.statusCode).json({
      error: exception.code,
      message: exception.message,
    });
  }
}

Apply via @UseFilters(MyDomainExceptionFilter) per controller/route, or globally:

app.useGlobalFilters(new MyDomainExceptionFilter());

Catch-all filter for logging

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);
  catch(exception: unknown, host: ArgumentsHost) {
    this.logger.error('unhandled', exception);
    // re-throw or format default response
  }
}

Add LAST in useGlobalFilters order (innermost catch-all).

Lifecycle hooks

@Injectable()
export class WarmupService implements OnModuleInit, OnApplicationShutdown {
  async onModuleInit() {
    // run after this module's providers instantiated
    await this.warmCache();
  }
  async onApplicationShutdown(signal?: string) {
    // close DB pool, drain queues
    this.logger.log(`shutting down: ${signal}`);
    await this.cleanup();
  }
}

Enable shutdown hooks in main:

const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();

Order:

  • OnModuleInit — after module's deps ready, before app boots.
  • OnApplicationBootstrap — after ALL modules ready.
  • OnModuleDestroy / OnApplicationShutdown — graceful shutdown on SIGTERM/SIGINT.

Logging

Built-in Logger:

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

  async create(dto: CreateUserDto) {
    this.logger.log(`creating user ${dto.email}`);
    try {
      // ...
    } catch (err) {
      this.logger.error('create failed', err);
      throw err;
    }
  }
}

For structured production logs, swap globally:

// option A: nestjs-pino
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(Logger));

// option B: custom logger implementing LoggerService
app.useLogger(new MyStructuredLogger());

Anti-patterns

  • ❌ new MyService() outside test files — always inject.
  • ❌ process.env.X in business code — use ConfigService.
  • ❌ Global ValidationPipe disabled "for performance" — measure first.
  • ❌ Catch-all try/catch swallowing errors silently.
  • ❌ Mutating injected services (this.someInjectedService.field = newValue).
  • ❌ Logic in controllers (anything beyond binding params and calling service).
  • ❌ Importing controllers from other modules.
  • ❌ @Global() on every shared module — defeats explicit-imports clarity.
  • ❌ Using forwardRef to "fix" a real coupling problem.
  • ❌ Module-level mutable state (no static class fields with non-readonly types).

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

AratKruglik のスキルをすべて見る

このスキルの問題を報告する