はじめに
本記事では、NestJSアプリケーションを設計・実装する際の主要な原則について解説します。
モジュラーアーキテクチャ
NestJSの核となる設計思想は、アプリケーションを機能的な単位(モジュール)に分割することです。
@Module({
imports: [
TypeOrmModule.forFeature([User]),
AuthModule,
ConfigModule,
],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
各モジュールは:
- 単一の責任を持つ
- 独立してテスト可能
- 再利用可能な単位として機能
依存性注入(DI)
DIはNestJSアプリケーションの設計の中心です:
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
private readonly configService: ConfigService,
) {}
}
DIの利点:
- 疎結合な設計
- テスタビリティの向上
- コードの再利用性
レイヤードアーキテクチャ
アプリケーションは明確に分離されたレイヤーで構成:
src/
├── controllers/ # プレゼンテーション層
├── services/ # ビジネスロジック層
├── repositories/ # データアクセス層
└── domain/ # ドメインモデル
実装例:
// Controller層(プレゼンテーション)
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}
// Service層(ビジネスロジック)
@Injectable()
export class UsersService {
constructor(private readonly usersRepository: UsersRepository) {}
async create(data: CreateUserDto): Promise<User> {
// ビジネスロジックの実装
return this.usersRepository.create(data);
}
}
// Repository層(データアクセス)
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User)
private readonly repository: Repository<User>,
) {}
async create(data: CreateUserDto): Promise<User> {
return this.repository.save(data);
}
}
型安全性とバリデーション
DTOとバリデーションデコレータを使用した堅牢な型システム:
export class CreateUserDto {
@IsString()
@IsNotEmpty()
readonly name: string;
@IsEmail()
readonly email: string;
@IsString()
@MinLength(8)
readonly password: string;
}
エラー処理
集中的なエラー処理メカニズム:
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
message: exception.message,
});
}
}
ベストプラクティス
1. 設定管理
@Injectable()
export class AppConfigService {
constructor(private configService: ConfigService) {}
get databaseUrl(): string {
return this.configService.get<string>('DATABASE_URL');
}
}
2. トランザクション管理
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
async createUserWithProfile(userData: CreateUserDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// トランザクション内の処理
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}
3. ガード(認証・認可)
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.get<boolean>(
'isPublic',
context.getHandler()
);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
実践的な注意点
-
モジュールの分割粒度
- 大きすぎず小さすぎない適切な粒度
- 機能的な凝集度の高さ
- 明確な責任範囲
-
テスタビリティ
- モックとスタブの適切な使用
- テストカバレッジの維持
- E2Eテストの重要性
-
パフォーマンス考慮
- N+1問題の回避
- キャッシング戦略
- 非同期処理の適切な使用
まとめ
これらの設計原則は、以下を実現するための基盤となります:
- メンテナンス性の高いコードベース
- スケーラブルなアプリケーション
- 堅牢なエラー処理
- 効率的なチーム開発
プロジェクトの要件や規模に応じて、これらの原則を適切に組み合わせることが重要です。
参考文献