백엔드 API 구현
NestJS를 사용한 백엔드 API 구현은 모듈화된 구조, 의존성 주입, 그리고 강력한 데코레이터 시스템을 활용하여 효율적이고 확장 가능한 애플리케이션을 만드는 과정입니다.
프로젝트 구조 설정
- NestJS CLI를 사용한 프로젝트 생성
nest new my-project
cd my-project
- 기본 프로젝트 구조
src/
├── app.module.ts
├── main.ts
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ └── dto/
├── auth/
│ ├── auth.module.ts
│ ├── auth.service.ts
│ └── jwt.strategy.ts
└── common/
├── filters/
├── interceptors/
└── pipes/
모듈, 컨트롤러, 서비스 구현
- 모듈 구현
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
- 컨트롤러 구현
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
}
- 서비스 구현
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findAll(): Promise<User[]> {
return this.usersRepository.find();
}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
}
데이터베이스 연동
TypeORM을 사용한 데이터베이스 연동
- 의존성 설치
npm install @nestjs/typeorm typeorm pg
- TypeORM 설정 (app.module.ts)
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'root',
password: 'password',
database: 'test',
entities: [User],
synchronize: true,
}),
],
})
export class AppModule {}
- 엔티티 정의
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}
인증 및 권한 부여
JWT를 사용한 인증 구현
- 의존성 설치
npm install @nestjs/jwt passport-jwt
- JWT 전략 구현
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'your-secret-key',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
- 가드 사용
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
미들웨어, 인터셉터, 파이프, 가드 활용
- 로깅 미들웨어
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: Function) {
console.log('Request...');
next();
}
}
- 변환 인터셉터
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => ({ data })));
}
}
- 유효성 검사 파이프
@Post()
create(@Body(new ValidationPipe()) createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
- 역할 기반 가드
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const roles = Reflect.getMetadata('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
에러 처리 및 로깅
전역 예외 필터 구현
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly logger: Logger) {}
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';
this.logger.error(
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : null,
'ExceptionFilter',
);
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
API 버저닝, 문서화, 테스팅
- API 버저닝
@Controller('api/v1/users')
export class UsersControllerV1 {}
@Controller('api/v2/users')
export class UsersControllerV2 {}
- Swagger를 사용한 API 문서화
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle('Users API')
.setDescription('The users API description')
.setVersion('1.0')
.addTag('users')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
- 테스팅
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
it('should return an array of users', async () => {
const result = [{ id: 1, name: 'Test User' }];
jest.spyOn(service, 'findAll').mockImplementation(() => result);
expect(await controller.findAll()).toBe(result);
});
});
성능 최적화
- 캐싱 구현
import { CacheInterceptor } from '@nestjs/cache-manager';
@UseInterceptors(CacheInterceptor)
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
- 데이터베이스 인덱싱
@Entity()
export class User {
@Index()
@Column()
email: string;
}
Best Practices 및 주의사항
- 단일 책임 원칙 준수 : 각 모듈, 서비스, 컨트롤러의 역할 명확히 분리
- 의존성 주입 활용 : 결합도 낮추고 테스트 용이성 증가
- DTO 사용 : 데이터 유효성 검증 및 타입 안정성 확보
- 비즈니스 로직은 서비스 계층에 구현
- 환경 변수 사용 : 설정 정보 외부화
- 에러 처리 일관성 유지
- 로깅 전략 수립 : 운영 환경에서의 문제 해결 용이성 확보
- 성능 모니터링 및 최적화 : 주기적인 성능 테스트 및 개선
- 보안 Best Practices 적용 : HTTPS, CORS 설정, 입력 유효성 검사 등
- 코드 품질 관리 : 린트 도구 사용, 코드 리뷰 프로세스 수립