NestJS
확장 가능한 서버 사이드 애플리케이션을 구축하기 위한 프로그레시브 TypeScript 우선 Node.js 프레임워크.
새 프로젝트 생성
섹션 제목: “새 프로젝트 생성”| 명령어 | 설명 |
|---|---|
npm i -g @nestjs/cli | NestJS CLI 전역 설치 |
nest new project-name | 새 NestJS 프로젝트 생성 |
nest new project-name --strict | 엄격한 TypeScript로 프로젝트 생성 |
nest new project-name -p pnpm | pnpm을 사용하여 프로젝트 생성 |
nest new project-name -p yarn | Yarn을 사용하여 프로젝트 생성 |
개발 명령어
섹션 제목: “개발 명령어”| 명령어 | 설명 |
|---|---|
npm run start:dev | 핫 리로드로 개발 서버 시작 |
npm run start:debug | 디버그 모드와 핫 리로드로 시작 |
npm run start:prod | 프로덕션 서버 시작 |
npm run build | 프로젝트 빌드 |
npm run test | 단위 테스트 실행 |
npm run test:watch | 감시 모드로 테스트 실행 |
npm run test:cov | 커버리지 리포트와 함께 테스트 실행 |
npm run test:e2e | 엔드투엔드 테스트 실행 |
npm run lint | 소스 코드 린트 |
프로젝트 구조
섹션 제목: “프로젝트 구조”src/
├── app.controller.ts # 루트 컨트롤러
├── app.controller.spec.ts # 컨트롤러 단위 테스트
├── app.module.ts # 루트 모듈
├── app.service.ts # 루트 서비스
├── main.ts # 애플리케이션 진입점
├── users/
│ ├── users.module.ts # 기능 모듈
│ ├── users.controller.ts # 기능 컨트롤러
│ ├── users.service.ts # 기능 서비스
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts
│ └── users.controller.spec.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.guard.ts
│ ├── auth.service.ts
│ └── strategies/
│ └── jwt.strategy.ts
└── common/
├── filters/
│ └── http-exception.filter.ts
├── interceptors/
│ └── logging.interceptor.ts
└── pipes/
└── validation.pipe.ts
CLI 코드 생성
섹션 제목: “CLI 코드 생성”생성 명령어
섹션 제목: “생성 명령어”| 명령어 | 설명 |
|---|---|
nest generate module users | 새 모듈 생성 |
nest generate controller users | 새 컨트롤러 생성 |
nest generate service users | 새 서비스 생성 |
nest generate resource users | 전체 CRUD 리소스 생성 (모듈 + 컨트롤러 + 서비스 + DTO) |
nest generate guard auth | 인증 가드 생성 |
nest generate pipe validation | 유효성 검사 파이프 생성 |
nest generate interceptor logging | 인터셉터 생성 |
nest generate middleware logger | 미들웨어 생성 |
nest generate filter http-exception | 예외 필터 생성 |
nest generate decorator roles | 커스텀 데코레이터 생성 |
nest generate class user.entity | 일반 클래스 생성 |
nest generate interface user | 인터페이스 생성 |
nest g res users --no-spec | 테스트 파일 없이 리소스 생성 |
nest g mo users --flat | 하위 폴더 없이 생성 |
nest info | 프로젝트 정보 및 의존성 표시 |
리소스 생성
섹션 제목: “리소스 생성”# 전체 CRUD 리소스 (가장 일반적인 워크플로우)
nest g resource users
# CLI 프롬프트:
# ? What transport layer do you use? REST API
# ? Would you like to generate CRUD entry points? Yes
# 생성되는 파일:
# src/users/users.module.ts
# src/users/users.controller.ts
# src/users/users.service.ts
# src/users/dto/create-user.dto.ts
# src/users/dto/update-user.dto.ts
# src/users/entities/user.entity.ts
# src/users/users.controller.spec.ts
# 업데이트: src/app.module.ts (UsersModule 임포트)
모듈 데코레이터
섹션 제목: “모듈 데코레이터”| 명령어 | 설명 |
|---|---|
@Module({ imports: [UsersModule] }) | 다른 모듈 임포트 |
@Module({ controllers: [UsersController] }) | 컨트롤러 등록 |
@Module({ providers: [UsersService] }) | 프로바이더/서비스 등록 |
@Module({ exports: [UsersService] }) | 다른 모듈용으로 프로바이더 내보내기 |
@Global() | 모듈을 전역으로 사용 가능하게 설정 (사용 자제) |
동적 모듈
섹션 제목: “동적 모듈”import { Module, DynamicModule } from '@nestjs/common'
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
global: true,
providers: [
{ provide: 'DATABASE_OPTIONS', useValue: options },
DatabaseService,
],
exports: [DatabaseService],
}
}
static forRootAsync(options: DatabaseAsyncOptions): DynamicModule {
return {
module: DatabaseModule,
global: true,
imports: options.imports || [],
providers: [
{
provide: 'DATABASE_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
DatabaseService,
],
exports: [DatabaseService],
}
}
}
// AppModule에서 사용
@Module({
imports: [
DatabaseModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
host: config.get('DB_HOST'),
port: config.get('DB_PORT'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
컨트롤러
섹션 제목: “컨트롤러”라우트 데코레이터
섹션 제목: “라우트 데코레이터”| 명령어 | 설명 |
|---|---|
@Controller('users') | 라우트 접두사로 컨트롤러 정의 |
@Get() | GET 요청 처리 |
@Post() | POST 요청 처리 |
@Put(':id') | 파라미터가 있는 PUT 요청 처리 |
@Patch(':id') | PATCH 요청 처리 |
@Delete(':id') | DELETE 요청 처리 |
@All('*') | 모든 HTTP 메서드 처리 |
파라미터 데코레이터
섹션 제목: “파라미터 데코레이터”| 명령어 | 설명 |
|---|---|
@Param('id') id: string | 라우트 파라미터 추출 |
@Param('id', ParseIntPipe) id: number | 정수로 추출 및 파싱 |
@Query('page') page: string | 쿼리 파라미터 추출 |
@Query() query: PaginationDto | 모든 쿼리 파라미터를 DTO로 추출 |
@Body() dto: CreateUserDto | 요청 본문 추출 및 타입 지정 |
@Body('email') email: string | 특정 본문 필드 추출 |
@Headers('authorization') auth: string | 요청 헤더 추출 |
@Ip() ip: string | 클라이언트 IP 주소 추출 |
@Req() request: Request | 원시 요청 객체 접근 |
@Res() response: Response | 원시 응답 객체 접근 |
@HttpCode(201) | 커스텀 HTTP 상태 코드 설정 |
@Header('Cache-Control', 'none') | 응답 헤더 설정 |
@Redirect('/new-url', 301) | 리다이렉트 응답 |
컨트롤러 예제
섹션 제목: “컨트롤러 예제”import {
Controller, Get, Post, Put, Delete, Param, Query, Body,
HttpCode, HttpStatus, ParseIntPipe, UseGuards, UseInterceptors,
} from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor'
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto)
}
@Get()
findAll(
@Query('page', new ParseIntPipe({ optional: true })) page = 1,
@Query('limit', new ParseIntPipe({ optional: true })) limit = 10,
) {
return this.usersService.findAll(page, limit)
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id)
}
@Put(':id')
@UseGuards(JwtAuthGuard)
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto)
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id)
}
}
서비스 & 프로바이더
섹션 제목: “서비스 & 프로바이더”의존성 주입
섹션 제목: “의존성 주입”| 명령어 | 설명 |
|---|---|
@Injectable() | 클래스를 주입 가능한 프로바이더로 표시 |
constructor(private usersService: UsersService) | 생성자를 통한 서비스 주입 |
@Inject('TOKEN') private config | 커스텀 토큰으로 주입 |
{ provide: 'TOKEN', useValue: value } | 값 프로바이더 등록 |
{ provide: 'TOKEN', useFactory: () => ... } | 팩토리 프로바이더 등록 |
{ provide: 'TOKEN', useClass: MyClass } | 클래스 프로바이더 등록 |
{ provide: 'TOKEN', useExisting: OtherService } | 기존 프로바이더 별칭 |
@Optional() private service?: MyService | 선택적 의존성 주입 |
{ provide: 'TOKEN', scope: Scope.REQUEST } | 요청 범위 프로바이더 |
서비스 예제
섹션 제목: “서비스 예제”import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepo: Repository<User>,
) {}
async create(dto: CreateUserDto): Promise<User> {
const exists = await this.usersRepo.findOneBy({ email: dto.email })
if (exists) {
throw new ConflictException('Email already registered')
}
const user = this.usersRepo.create(dto)
return this.usersRepo.save(user)
}
async findAll(page: number, limit: number) {
const [items, total] = await this.usersRepo.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
})
return { items, total, page, limit }
}
async findOne(id: number): Promise<User> {
const user = await this.usersRepo.findOneBy({ id })
if (!user) throw new NotFoundException(`User #${id} not found`)
return user
}
async update(id: number, dto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id)
Object.assign(user, dto)
return this.usersRepo.save(user)
}
async remove(id: number): Promise<void> {
const result = await this.usersRepo.delete(id)
if (result.affected === 0) {
throw new NotFoundException(`User #${id} not found`)
}
}
}
유효성 검사 & DTO
섹션 제목: “유효성 검사 & DTO”Class Validator 설정
섹션 제목: “Class Validator 설정”npm install class-validator class-transformer
// main.ts - 전역 유효성 검사 활성화
import { ValidationPipe } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 알 수 없는 속성 제거
forbidNonWhitelisted: true, // 알 수 없는 속성에 대해 에러 발생
transform: true, // 페이로드를 DTO 타입으로 자동 변환
transformOptions: {
enableImplicitConversion: true, // 쿼리 문자열을 숫자/불리언으로 변환
},
}))
await app.listen(3000)
}
DTO 예제
섹션 제목: “DTO 예제”import {
IsString, IsEmail, IsOptional, IsInt, Min, Max,
MinLength, MaxLength, IsEnum, ValidateNested, IsArray,
} from 'class-validator'
import { Type } from 'class-transformer'
import { PartialType, OmitType, PickType, IntersectionType } from '@nestjs/mapped-types'
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(50)
name: string
@IsEmail()
email: string
@IsString()
@MinLength(8)
password: string
@IsOptional()
@IsEnum(Role)
role?: Role
@IsOptional()
@ValidateNested()
@Type(() => AddressDto)
address?: AddressDto
}
// 업데이트 DTO 자동 생성 (모든 필드 선택적)
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// 특정 필드 선택
export class LoginDto extends PickType(CreateUserDto, ['email', 'password']) {}
// 특정 필드 제외
export class PublicUserDto extends OmitType(CreateUserDto, ['password']) {}
가드 & 인증
섹션 제목: “가드 & 인증”가드 설정
섹션 제목: “가드 설정”| 명령어 | 설명 |
|---|---|
@UseGuards(AuthGuard) | 컨트롤러 또는 라우트에 가드 적용 |
@UseGuards(AuthGuard, RolesGuard) | 여러 가드 체이닝 |
app.useGlobalGuards(new AuthGuard()) | 전역 가드 등록 |
canActivate(context: ExecutionContext) | 가드 로직 구현 |
@SetMetadata('roles', ['admin']) | 가드용 커스텀 메타데이터 설정 |
Reflector | 가드에서 메타데이터 읽기 |
JWT 인증
섹션 제목: “JWT 인증”npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
// auth/auth.module.ts
@Module({
imports: [
UsersModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
inject: [ConfigService],
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('JWT_SECRET'),
})
}
validate(payload: { sub: number; email: string }) {
return { id: payload.sub, email: payload.email }
}
}
// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// 사용법
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
@Get()
getProfile(@Req() req) {
return req.user // JwtStrategy.validate()에 의해 주입됨
}
}
역할 기반 접근 제어
섹션 제목: “역할 기반 접근 제어”// roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
])
if (!requiredRoles) return true
const { user } = context.switchToHttp().getRequest()
return requiredRoles.some(role => user.roles?.includes(role))
}
}
// 사용법
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('dashboard')
@Roles('admin')
getDashboard() {
return { message: 'Admin dashboard' }
}
@Delete('users/:id')
@Roles('admin', 'superadmin')
removeUser(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id)
}
}
미들웨어, 인터셉터 & 파이프
섹션 제목: “미들웨어, 인터셉터 & 파이프”미들웨어
섹션 제목: “미들웨어”import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now()
res.on('finish', () => {
const duration = Date.now() - start
console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`)
})
next()
}
}
// 모듈에서 등록
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*')
}
}
인터셉터
섹션 제목: “인터셉터”import {
Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common'
import { Observable, map, tap } from 'rxjs'
// 응답 형태 변환
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(context: ExecutionContext, next: CallHandler): Observable<{ data: T }> {
return next.handle().pipe(
map(data => ({ data, timestamp: new Date().toISOString() })),
)
}
}
// 실행 시간 측정
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now()
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start
const response = context.switchToHttp().getResponse()
response.setHeader('X-Response-Time', `${duration}ms`)
}),
)
}
}
데이터베이스 통합
섹션 제목: “데이터베이스 통합”TypeORM 설정
섹션 제목: “TypeORM 설정”| 명령어 | 설명 |
|---|---|
npm install @nestjs/typeorm typeorm pg | PostgreSQL과 함께 TypeORM 설치 |
TypeOrmModule.forRoot({ type: 'postgres', ... }) | AppModule에서 연결 설정 |
TypeOrmModule.forFeature([User]) | 기능 모듈에서 엔티티 등록 |
@InjectRepository(User) private repo: Repository<User> | 리포지토리 주입 |
Prisma 설정
섹션 제목: “Prisma 설정”npm install @prisma/client
npm install -D prisma
npx prisma init
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect()
}
async onModuleDestroy() {
await this.$disconnect()
}
}
// prisma/prisma.module.ts
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
// 서비스에서 사용
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
findAll() {
return this.prisma.user.findMany()
}
findOne(id: number) {
return this.prisma.user.findUniqueOrThrow({ where: { id } })
}
}
예외 처리
섹션 제목: “예외 처리”내장 예외
섹션 제목: “내장 예외”| 명령어 | 설명 |
|---|---|
throw new BadRequestException('Invalid input') | 400 잘못된 요청 |
throw new UnauthorizedException('Login required') | 401 인증 필요 |
throw new ForbiddenException('Access denied') | 403 접근 거부 |
throw new NotFoundException('User not found') | 404 찾을 수 없음 |
throw new ConflictException('Email taken') | 409 충돌 |
throw new UnprocessableEntityException('Validation failed') | 422 처리 불가 |
throw new InternalServerErrorException() | 500 내부 서버 오류 |
커스텀 예외 필터
섹션 제목: “커스텀 예외 필터”import {
ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
} from '@nestjs/common'
import { Request, Response } from 'express'
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR
const message = exception instanceof HttpException
? exception.getResponse()
: 'Internal server error'
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: typeof message === 'string' ? message : (message as any).message,
})
}
}
// main.ts에서 전역으로 등록
app.useGlobalFilters(new AllExceptionsFilter())
환경 설정
섹션 제목: “환경 설정”npm install @nestjs/config
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(3000),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
}),
}),
],
})
export class AppModule {}
// 모든 서비스에서 사용
@Injectable()
export class AppService {
constructor(private config: ConfigService) {}
getPort(): number {
return this.config.get<number>('PORT', 3000)
}
}
테스트
섹션 제목: “테스트”단위 테스트
섹션 제목: “단위 테스트”import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { getRepositoryToken } from '@nestjs/typeorm'
import { User } from './entities/user.entity'
describe('UsersService', () => {
let service: UsersService
const mockRepository = {
create: jest.fn(),
save: jest.fn(),
findOneBy: jest.fn(),
findAndCount: jest.fn(),
delete: jest.fn(),
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepository },
],
}).compile()
service = module.get<UsersService>(UsersService)
})
it('should create a user', async () => {
const dto = { name: 'Alice', email: 'alice@example.com', password: 'secret123' }
mockRepository.findOneBy.mockResolvedValue(null)
mockRepository.create.mockReturnValue(dto)
mockRepository.save.mockResolvedValue({ id: 1, ...dto })
const result = await service.create(dto)
expect(result.id).toBe(1)
expect(mockRepository.save).toHaveBeenCalled()
})
})
E2E 테스트
섹션 제목: “E2E 테스트”import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'
describe('UsersController (e2e)', () => {
let app: INestApplication
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleFixture.createNestApplication()
app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
await app.init()
})
afterAll(async () => {
await app.close()
})
it('/users (POST) creates a user', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'Alice', email: 'alice@example.com', password: 'secret123' })
.expect(201)
.expect((res) => {
expect(res.body.name).toBe('Alice')
expect(res.body.id).toBeDefined()
})
})
it('/users (GET) returns paginated list', () => {
return request(app.getHttpServer())
.get('/users?page=1&limit=10')
.expect(200)
.expect((res) => {
expect(res.body.items).toBeInstanceOf(Array)
expect(res.body.total).toBeDefined()
})
})
})
모범 사례
섹션 제목: “모범 사례”-
리소스 생성기 사용 —
nest g resource는 전체 CRUD 스캐폴드 (모듈, 컨트롤러, 서비스, DTO, 엔티티, 테스트)를 몇 초 만에 생성하고 프로젝트 일관성을 유지합니다. -
whitelist로 전역 ValidationPipe 활성화 —
whitelist: true와forbidNonWhitelisted: true를 설정하여 모든 요청에서 예상치 못한 필드를 자동으로 제거하거나 거부합니다. -
DTO에 매핑 타입 사용 —
PartialType,PickType,OmitType,IntersectionType은 생성 DTO에서 업데이트/쿼리 DTO를 파생하여 DRY를 유지합니다. -
생성자 주입 선호 — 커스텀 프로바이더가 필요하지 않은 한 수동
@Inject()토큰 대신 생성자를 통한 의존성 주입을 사용합니다. -
컨트롤러를 가볍게 유지 — 컨트롤러는 입력 파싱과 출력 반환만 해야 합니다. 테스트 가능성과 재사용을 위해 모든 비즈니스 로직을 서비스로 이동합니다.
-
모든 환경 접근에 ConfigModule 사용 — 서비스에서
process.env를 직접 읽지 마세요. 유효성 검사 스키마와 함께ConfigService를 사용하여 누락된 변수가 시작 시 빠르게 실패하도록 합니다. -
가드와 인터셉터의 적절한 범위 지정 — 정말로 모든 곳에 적용되지 않는 한 전역이 아닌 데코레이터로 컨트롤러 또는 라우트 수준에서 적용합니다.
-
단위 및 E2E 테스트 작성 —
Test.createTestingModule()로 모의 의존성을 사용하여 서비스를 단위 테스트하고,supertest로 HTTP 엔드포인트를 E2E 테스트합니다. -
Prisma 또는 TypeORM을 일관되게 사용 — 하나의 ORM을 선택하여 프로젝트 전체에서 사용합니다. Prisma는 더 나은 타입 안전성을, TypeORM은 데코레이터로 더 많은 유연성을 제공합니다.
-
타입이 아닌 기능별로 구조화 — 타입별(controllers/, services/)이 아닌 도메인별(users/, auth/, orders/)로 파일을 그룹화합니다. 이렇게 하면 관련 코드가 함께 유지되고 모듈이 자체 포함됩니다.
-
예외로 오류 처리 — 서비스에서 내장 HTTP 예외(
NotFoundException,ConflictException)를 던집니다. 커스텀 오류 형식에는 예외 필터를 사용합니다. -
async/await를 일관되게 사용 — NestJS는 기본적으로 프로미스를 처리합니다. 서비스 메서드에서 비동기 값을 반환하고 프레임워크가 응답을 직렬화하도록 합니다.