NestJS 開発パターン は何をしますか?
本番グレードのNestJSパターンによるモジュラーなTypeScriptバックエンド。
アクティベーションのタイミング
- NestJS APIやサービスの構築時
- モジュール、コントローラ、プロバイダの構造化時
- DTOバリデーション、ガード、インターセプタ、例外フィルタの追加時
- 環境依存の設定やデータベース統合の構成時
- NestJSのユニットテストやHTTPエンドポイントのテスト時
プロジェクト構造
src/
├── app.module.ts
├── main.ts
├── common/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
├── config/
│ ├── configuration.ts
│ └── validation.ts
├── modules/
│ ├── auth/
│ │ ├── auth.controller.ts
│ │ ├── auth.module.ts
│ │ ├── auth.service.ts
│ │ ├── dto/
│ │ ├── guards/
│ │ └── strategies/
│ └── users/
│ ├── dto/
│ ├── entities/
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
└── prisma/ or database/
- ドメインコードはフィーチャーモジュール内に保持します。
- クロスカッティングなフィルタ、デコレータ、ガード、インターセプタは
common/に配置します。 - DTOはそれを所有するモジュールの近くに置きます。
ブートストラップとグローバルバリデーション
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
- 公開APIでは常に
whitelistとforbidNonWhitelistedを有効にします。 - ルートごとにバリデーション設定を繰り返す代わりに、1つのグローバルバリデーションパイプを使用することを推奨します。
モジュール、コントローラ、プロバイダ
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
getById(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.getById(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
@Injectable()
export class UsersService {
constructor(private readonly usersRepo: UsersRepository) {}
async create(dto: CreateUserDto) {
return this.usersRepo.create(dto);
}
}
- コントローラは薄く保ちます。HTTP入力を解析し、プロバイダを呼び出し、レスポンスDTOを返すだけにします。
- ビジネスロジックはコントローラではなく、インジェクタブルなサービスに置きます。
- 他のモジュールが本当に必要とするプロバイダのみをエクスポートします。
DTOとバリデーション
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@Length(2, 80)
name!: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
- すべてのリクエストDTOを
class-validatorでバリデーションします。 - ORMエンティティを直接返す代わりに、専用のレスポンスDTOやシリアライザを使用します。
- パスワードハッシュ、トークン、監査列などの内部フィールドを漏洩させないようにします。
認証、ガード、リクエストコンテキスト
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin/report')
getAdminReport(@Req() req: AuthenticatedRequest) {
return this.reportService.getForUser(req.user.id);
}
- 認証ストラテジとガードは、真に共有される場合を除き、モジュールローカルに保ちます。
- アクセスルールの大枠をガードでエンコードし、リソース固有の認可はサービス内で行います。
- 認証済みリクエストオブジェクトには明示的なリクエスト型を推奨します。
例外フィルタとエラー形式
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
const request = host.switchToHttp().getRequest<Request>();
if (exception instanceof HttpException) {
return response.status(exception.getStatus()).json({
path: request.url,
error: exception.getResponse(),
});
}
return response.status(500).json({
path: request.url,
error: 'Internal server error',
});
}
}
- API全体で一貫した1つのエラーエンベロープを維持します。
- 期待されるクライアントエラーにはフレームワークの例外をスローし、予期しない障害は中央でログ出力してラップします。
設定と環境バリデーション
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
validate: validateEnv,
});
- 環境変数は起動時にバリデーションし、最初のリクエストで遅延評価しないようにします。
- 設定へのアクセスは、型付けされたヘルパーまたは設定サービスの背後に隠します。
- 開発/ステージング/本番の設定の違いは、フィーチャーコード全体に条件分岐を散りばめるのではなく、設定ファクトリで分割します。
永続化とトランザクション
- リポジトリ/ORMコードは、ドメイン言語で話すプロバイダの背後に隠します。
- PrismaやTypeORMの場合、トランザクショナルなワークフローは、その作業単位を所有するサービス内に隔離します。
- コントローラが直接マルチステップの書き込みを調整することを禁止します。
テスト
describe('UsersController', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [UsersModule],
}).compile();
app = moduleRef.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.init();
});
});
- プロバイダは依存関係をモックして単体テストを行います。
- ガード、バリデーションパイプ、例外フィルタについてはリクエストレベルのテストを追加します。
- テストでは本番環境と同じグローバルパイプ/フィルタを再利用します。
本番環境のデフォルト設定
- 構造化ログとリクエスト相関IDを有効にします。
- 環境変数/設定が無効な場合は、部分的に起動するのではなく終了します。
- DB/キャッシュクライアントの非同期プロバイダ初期化を明示的なヘルスチェックと共に推奨します。
- バックグラウンドジョブやイベントコンシューマは、HTTPコントローラ内ではなく、独自のモジュールに配置します。
- 公開エンドポイントでは、レート制限、認証、監査ログを明示的にします。