| name | nestjs-patterns |
| description | 모듈, 컨트롤러, 프로바이더, DTO 검증, 가드, 인터셉터, 설정, 운영 수준 TypeScript 백엔드를 위한 NestJS 아키텍처 패턴입니다. |
| origin | ECC |
NestJS 개발 패턴
모듈형 TypeScript 백엔드를 위한 운영 수준 NestJS 패턴입니다.
사용 시점
- 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를 항상 켭니다.
- 라우트마다 검증 설정을 반복하기보다 전역 validation pipe 하나를 우선합니다.
모듈, 컨트롤러, 프로바이더
@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 () {
..(dto);
}
}
- 컨트롤러는 얇게 유지합니다. HTTP 입력을 파싱하고, 프로바이더를 호출하고, 응답 DTO를 반환하는 정도에 그쳐야 합니다.
- 비즈니스 로직은 컨트롤러가 아니라 주입 가능한 서비스에 둡니다.
- 다른 모듈이 실제로 필요한 프로바이더만 export합니다.
DTO와 검증
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@Length(2, 80)
name!: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
- 모든 요청 DTO는
class-validator로 검증합니다.
- ORM 엔티티를 직접 반환하지 말고 전용 응답 DTO나 serializer를 사용합니다.
- 비밀번호 해시, 토큰, 감사 컬럼 같은 내부 필드가 노출되지 않게 합니다.
인증, 가드, 요청 컨텍스트
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin/report')
getAdminReport(@Req() req: AuthenticatedRequest) {
return this.reportService.getForUser(req.user.id);
}
- 인증 전략과 가드는 정말 공유되는 경우가 아니면 모듈 내부에 둡니다.
- 거친 접근 규칙은 가드에 두고, 리소스별 권한 검사는 서비스에서 처리합니다.
- 인증된 요청 객체에는 명시적 request 타입을 사용합니다.
예외 필터와 오류 형식
@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 전반에서 일관된 오류 envelope를 유지합니다.
- 예상 가능한 클라이언트 오류는 프레임워크 예외를 던지고, 예상치 못한 실패는 중앙에서 로깅하고 감쌉니다.
설정과 환경 검증
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
validate: validateEnv,
});
- 환경 변수는 첫 요청 시점이 아니라 부팅 시점에 검증합니다.
- 설정 접근은 타입이 있는 헬퍼나 config service 뒤에 둡니다.
- 개발/스테이징/운영 분기는 기능 코드 곳곳이 아니라 config factory에서 분리합니다.
영속성과 트랜잭션
- 저장소/ORM 코드는 도메인 언어를 말하는 프로바이더 뒤에 둡니다.
- Prisma나 TypeORM에서는 unit of work를 소유한 서비스 안에 트랜잭션 워크플로를 격리합니다.
- 다단계 쓰기 작업을 컨트롤러가 직접 조정하게 하지 않습니다.
테스트
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();
});
});
- 프로바이더는 mock 의존성과 함께 격리된 단위 테스트를 합니다.
- 가드, validation pipe, 예외 필터에는 요청 수준 테스트를 추가합니다.
- 테스트에서도 운영과 같은 전역 pipe/filter를 재사용합니다.
운영 기본값
- 구조화 로깅과 요청 상관관계 ID를 활성화합니다.
- 잘못된 env/config 상태에서는 부분 부팅하지 말고 종료합니다.
- DB/캐시 클라이언트는 명시적 헬스체크와 함께 async provider 초기화를 우선합니다.
- 백그라운드 작업과 이벤트 소비자는 HTTP 컨트롤러 안이 아니라 별도 모듈에 둡니다.
- 공개 엔드포인트에는 rate limiting, auth, audit logging을 명시적으로 적용합니다.