src/common/decorator/swagger.decorator.ts
에서 공통으로 사용할 수 있는 커스텀 Swagger Decorator를 생성했습니다.
import { applyDecorators, Type } from '@nestjs/common';
import { ApiCreatedResponse, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
import { PageResDto } from '../dto/res.dto';
export const ApiGetResponse = <TModel extends Type<any>>(model: TModel) => {
return applyDecorators(
ApiOkResponse({
schema: {
allOf: [{ $ref: getSchemaPath(model) }],
},
}),
);
};
export const ApiPostResponse = <TModel extends Type<any>>(model: TModel) => {
return applyDecorators(
ApiCreatedResponse({
schema: {
allOf: [{ $ref: getSchemaPath(model) }],
},
}),
);
};
export const ApiGetItemsResponse = <TModel extends Type<any>>(model: TModel) => {
return applyDecorators(
ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(PageResDto) },
{
properties: {
items: {
type: 'array',
items: { $ref: getSchemaPath(model) },
},
},
required: ['items'],
},
],
},
}),
);
};
설명:
applyDecorators
: 여러 데코레이터를 하나로 결합하는 유틸리티 함수ApiGetResponse
: GET 요청의 응답 스키마를 정의 (200 OK)ApiPostResponse
: POST 요청의 응답 스키마를 정의 (201 Created)ApiGetItemsResponse
: 페이지네이션이 포함된 배열 응답 스키마를 정의@ApiTags('User')
@ApiExtraModels(FindUserReqDto, FindUserResDto, PageResDto)
@Controller('api/users')
export class UserController {
@ApiBearerAuth()
@ApiGetItemsResponse(FindUserResDto)
@Get()
findAll(@Query() { page, size }: PageReqDto, @User() user: UserAfterAuth) {
return this.userService.findAll();
}
@ApiBearerAuth()
@ApiGetResponse(FindUserResDto)
@Get(':id')
findOne(@Param() { id }: FindUserReqDto) {
return this.userService.findOne(id);
}
}
사용된 Decorator들:
@ApiTags('User')
: Swagger UI에서 API 그룹을 'User'로 분류@ApiExtraModels()
: Body가 아닌 파라미터의 스키마를 Swagger에 추가@ApiBearerAuth()
: JWT 인증이 필요한 엔드포인트임을 표시 (잠금 표시)@ApiGetResponse()
, @ApiGetItemsResponse()
: 커스텀 응답 스키마 적용export class SignupReqDto {
@ApiProperty({ required: true, example: 'nestjs@fastcampus.com' })
@IsEmail()
@MaxLength(30)
email: string;
@ApiProperty({ required: true, example: 'Password1!' })
@Matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{10,30}$/)
password: string;
}
package.json
에서 필요한 패키지들이 설치되어 있습니다:
{
"dependencies": {
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0"
}
}
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// ValidationPipe 전역 적용
app.useGlobalPipes(
new ValidationPipe({
// class-transformer 적용
transform: true,
}),
);
}
설명:
ValidationPipe
: 요청 데이터의 유효성 검사를 자동으로 수행transform: true
: 요청 데이터를 DTO 클래스의 인스턴스로 자동 변환export class PageReqDto {
@ApiPropertyOptional({ description: '페이지. Default = 1' })
@Transform(({ value }) => Number(value))
@IsInt()
page?: number = 1;
@ApiPropertyOptional({ description: '페이지당 데이터 갯수. Default = 50' })
@Transform(({ value }) => Number(value))
@IsInt()
size?: number = 50;
}
사용된 Decorator들:
@Transform()
: 문자열로 전달된 쿼리 파라미터를 숫자로 변환@IsInt()
: 정수인지 검증@ApiPropertyOptional()
: 선택적 속성임을 Swagger에 표시src/auth/auth.module.ts
에서 JWT 모듈을 등록합니다:
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
global: true,
secret: 'temp secret',
signOptions: { expiresIn: '1d' },
}),
],
providers: [
AuthService,
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AuthModule {}
설명:
JwtModule.register()
: JWT 서비스 설정global: true
: 전역적으로 사용 가능secret
: JWT 토큰 서명에 사용할 비밀키expiresIn: '1d'
: 토큰 만료 시간 (1일)APP_GUARD
: 전역 가드로 JwtAuthGuard 등록src/auth/jwt.strategy.ts
에서 JWT 토큰 검증 로직을 구현합니다:
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'temp secret',
});
}
// request.user
async validate(payload: any) {
return { id: payload.sub };
}
}
설명:
ExtractJwt.fromAuthHeaderAsBearerToken()
: Authorization 헤더에서 Bearer 토큰 추출validate()
: JWT 페이로드를 검증하고 사용자 정보 반환payload.sub
: JWT 토큰의 subject (보통 사용자 ID)src/auth/jwt-auth.guard.ts
에서 인증 가드를 구현합니다:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
설명:
AuthGuard('jwt')
: Passport의 JWT 전략을 사용하는 가드Reflector
: 메타데이터를 읽기 위한 유틸리티IS_PUBLIC_KEY
: Public 데코레이터가 설정된 엔드포인트는 인증 없이 접근 가능@ApiTags('User')
@Controller('api/users')
export class UserController {
@ApiBearerAuth()
@ApiGetItemsResponse(FindUserResDto)
@Get()
findAll(@Query() { page, size }: PageReqDto, @User() user: UserAfterAuth) {
return this.userService.findAll();
}
}
설명:
@ApiBearerAuth()
: Swagger UI에서 Bearer 토큰 인증이 필요함을 표시 (잠금 아이콘)@UseGuards(JwtAuthGuard)
를 명시하지 않아도 됨const config = new DocumentBuilder()
.setTitle('NestJS project')
.setDescription('NestJS project API description')
.setVersion('1.0')
.addBearerAuth()
.build();
설명:
addBearerAuth()
: Swagger UI에서 Bearer 토큰 인증 기능 활성화src/common/decorator/public.decorator.ts
에서 Public 엔드포인트를 위한 데코레이터를 생성합니다:
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
설명:
SetMetadata
: 메타데이터를 설정하는 유틸리티IS_PUBLIC_KEY
: 메타데이터 키로 사용할 상수@Public()
: 해당 엔드포인트가 인증 없이 접근 가능함을 표시@ApiTags('Auth')
@Controller('api/auth')
export class AuthController {
@ApiPostResponse(SignupResDto)
@Public()
@Post('signup')
async signup(@Body() { email, password, passwordConfirm }: SignupReqDto) {
// 인증 없이 접근 가능
}
@ApiPostResponse(SigninResDto)
@Public()
@Post('signin')
async signin(@Body() { email, password }: SigninReqDto) {
// 인증 없이 접근 가능
}
}
설명:
@Public()
: 전역 JWT 가드가 적용되어 있어도 이 엔드포인트는 인증 없이 접근 가능src/common/decorator/user.decorator.ts
에서 인증된 사용자 정보를 쉽게 추출할 수 있는 데코레이터를 생성합니다:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
});
export interface UserAfterAuth {
id: string;
}
설명:
createParamDecorator
: 커스텀 파라미터 데코레이터 생성request.user
: JWT Strategy의 validate() 메서드에서 반환된 사용자 정보UserAfterAuth
: 인증 후 사용자 정보의 타입 정의@Controller('api/users')
export class UserController {
@Get()
findAll(@Query() { page, size }: PageReqDto, @User() user: UserAfterAuth) {
console.log(user); // { id: "user-id" }
return this.userService.findAll();
}
}
설명:
@User()
: 인증된 사용자의 정보를 자동으로 추출const customOptions: SwaggerCustomOptions = {
swaggerOptions: {
persistAuthorization: true,
},
};
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, customOptions);
설명:
persistAuthorization: true
: Swagger UI에서 페이지 새로고침 시에도 인증 정보 유지이 프로젝트에서는 다음과 같은 Decorator 패턴들을 활용했습니다:
이러한 패턴들을 통해 코드의 재사용성을 높이고, 일관된 API 문서화와 인증 시스템을 구축할 수 있습니다.