7. Pipes
파이프는 @Injectable() 데코레이터로 주석이 달린 클래스로, 파이프 트랜스폼 인터페이스를 구현합니다.
파이프는 두가지 일반적인 사용 사례가 있습니다.
- 변환: 입력 데이터를 원하는 형태로 변환(예: 문자열에서 정수로)
- 유효성 검사: 입력 데이터를 평가하여 유효하면 변경하지 않고 통과시키고, 그렇지 않으면 예외를 던짐
두 경우 모두 파이프는 컨트롤러 라우트 핸들러가 처리중인 인수를 대상으로 작동합니다. Nest는 메서드가 호출되기 직전에 파이프를 삽입하고, 파이프는 메서드의 대상이 되는 인수를 받아 이를 대상으로 작업합니다. 이때 모든 변환 또는 유효성 검사 작업이 수행되고, 그 후에 라우트 핸들러가 (잠재적으로) 변환된 인수를 사용하여 호출됩니다.
Nest에는 바로 사용할 수 있는 여러 가지 기본 제공 파이프가 있습니다. 사용자 정의 파이프를 직접 구축할 수도 있습니다. 이 장에서는 기본 제공 파이프를 소개하고 이를 라우트 핸들러에 바인딩하는 방법을 보여드리겠습니다. 그런 다음 몇 가지 사용자 정의 파이프를 살펴보고 처음부터 파이프를 구축하는 방법을 보여드리겠습니다.
힌트:
파이프는 예외 영역 내에서 실행됩니다. 즉, 파이프가 예외를 던지면 예외 레이어(전역 예외 필터 및 현재 컨텍스트에 적용되는 모든 예외 필터)에서 처리됩니다. 위의 내용을 고려할 때, 파이프에서 예외가 발생하면 컨트롤러 메서드가 이후에 실행되지 않는다는 것을 분명히 알 수 있습니다. 이는 시스템 경계에서 외부 소스에서 애플리케이션으로 들어오는 데이터의 유효성을 검사하는 모범 사례 기법을 제공합니다.
Built-in pipes
Nest에는 즉시 사용 가능한 여러 개의 파이프가 제공됩니다.
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
ParseDatePipe
이것들은 @nestjs/common
패키지에서 export 됩니다.
ParseIntPipe 사용에 대해 간단히 살펴보겠습니다. 이것은 파이프가 메서드 핸들러 매개변수가 자바스크립트 정수로 변환되도록 하거나 변환에 실패하면 예외를 던지는 변환 사용 사례의 예시입니다. 이 장의 뒷부분에서 ParseIntPipe에 대한 간단한 사용자 정의 구현을 보여드리겠습니다. 아래의 예제 기법은 다른 기본 제공 변환 파이프(이 장에서 Parse* 파이프라고 부르는 ParseBoolPipe, ParseFloatPipe, ParseEnumPipe, ParseArrayPipe, ParseDatePipe 및 ParseUUIDPipe)에도 적용될 수 있습니다.
Binding pipes
파이프를 사용하려면 파이프 클래스의 인스턴스를 적절한 컨텍스트에 바인딩해야 합니다. 파스인트파이프 예제에서는 파이프를 특정 라우트 핸들러 메서드와 연결하고 메서드가 호출되기 전에 파이프가 실행되도록 하려고 합니다. 이를 위해 메서드 매개변수 수준에서 파이프를 바인딩하는 다음 구문을 사용합니다:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
이렇게 하면 findOne() 메서드에서 받은 매개변수가 숫자(this.catsService.findOne() 호출에서 예상한 대로)이거나 경로 핸들러가 호출되기 전에 예외가 발생하는 두 가지 조건 중 하나가 참인지 확인할 수 있습니다. 예를 들어 경로가 다음과 같이 호출된다고 가정해 보겠습니다:
GET localhost:3000/abc
Nest는 다음과 같은 예외를 던집니다.
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
예외는 findOne() 메서드의 본문이 실행되지 않도록 합니다. 위의 예에서는 인스턴스가 아닌 클래스(ParseIntPipe)를 전달하여 인스턴스화에 대한 책임을 프레임워크에 맡기고 의존성 주입을 가능하게 합니다. 파이프 및 가드와 마찬가지로, 대신 제자리에 있는 인스턴스를 전달할 수 있습니다. 인플레이스 인스턴스 전달은 옵션을 전달하여 내장된 파이프의 동작을 사용자 정의하려는 경우에 유용합니다:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
다른 변환 파이프(모든 Parse* 파이프)를 바인딩하는 것도 비슷하게 작동합니다. 이러한 파이프는 모두 경로 매개변수, 쿼리 문자열 매개변수 및 요청 본문 값의 유효성을 검사하는 컨텍스트에서 작동합니다.
예를 들어 쿼리 문자열 매개변수가 있습니다:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
다음은 문자열 매개변수를 구문 분석하고 UUID인지 확인하는 데 ParseUUIDPipe를 사용하는 예제입니다.
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
힌트:
ParseUUIDPipe()를 사용할 때 버전 3, 4 또는 5의 UUID를 구문 분석하는 경우 특정 버전의 UUID만 필요한 경우 파이프 옵션에서 버전을 전달할 수 있습니다. 위에서 다양한 Parse* 기본 제공 파이프 제품군을 바인딩하는 예제를 살펴보았습니다. 유효성 검사 파이프를 바인딩하는 것은 조금 다르므로 다음 섹션에서 설명하겠습니다.
힌트 또한 유효성 검사 파이프에 대한 광범위한 예제는 유효성 검사 기술을 참조하세요.
Custom pipes
앞서 언급했듯이 자신만의 사용자 정의 파이프를 구축할 수 있습니다. Nest는 강력한 기본 제공 ParseIntPipe와 ValidationPipe를 제공하지만, 사용자 정의 파이프가 어떻게 구성되는지 알아보기 위해 각각의 간단한 사용자 정의 버전을 처음부터 만들어 보겠습니다.
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
힌트:
PipeTransform<T, R>
은 모든 파이프에서 구현해야 하는 일반 인터페이스입니다. 이 일반 인터페이스는 T를 사용하여 입력 값의 유형을 나타내고 R을 사용하여 transform() 메서드의 반환 유형을 나타냅니다.
모든 파이프는 PipeTransform
인터페이스의 조건을 만족시키기 위해 transform()
메서드를 구현해야 합니다. 해당 메서드는 아래의 두 가지 매개변수를 가지고 있습니다.
value
metadata
value
는 라우트 핸들러에 넘겨주기 전의, 현재 처리되는 메서드에 들어오는 인수(argument)입니다. 또, metadata
는 현재 처리되는 메서드 인수의 메타데이터입니다. 메타데이터 객체는 아래의 속성들을 갖습니다.
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
위 속성들은 현재 처리되는 인수들을 나타냅니다.
속성 | 설명 |
---|---|
type |
인수가 @Body() 인지, @Query() 인지, @Param() 인지, 혹은 사용자 지정 매개변수인지를 나타냅니다. 더 자세한 내용은 여기를 참고해주세요. |
metatype |
String 처럼 인수의 메타타입을 나타냅니다. 이 값은 라우트 메서드의 시그니처에 타입 선언이 되어있지 않거나, 바닐라 자바스크립트를 쓸 때 undefined 이 됩니다. |
data |
@Body('string') 에서 'string' 처럼 데코레이터에 전달된 문자열을 나타냅니다. 아무 값도 안 넣어주면 undefined 이 됩니다. |
주의
타입스크립트의 인터페이스는 트랜스파일 과정에서 사라집니다. 그러므로, 메서드 매개변수의 타입이 클래스 대신에 인터페이스로 선언되어 있다면
metatype
의 값은Object
가 됩니다.
Schema based validation
검증 파이프를 더 쓸모 있게 만들어봅시다. CatsController
의 create()
메서드를 더 자세히 살펴보세요! 여기서, 서비스의 메서드를 실행하기 전에 POST의 바디 객체가 유효한지 확인하려 합니다.
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
위 코드에서, 바디 매개변수 createCatDto
의 타입은 CreateCatDto
클래스입니다. 해당 클래스는 아래와 같이 생겼습니다.
// create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
create
메서드로 들어오는 요청이 유효한 바디를 갖는 걸 보장하게 하려면, createCatDto
객체의 세 멤버를 확인해봐야 합니다. 물론, 라우트 핸들러 메서드 안에서 처리할 수도 있겠지만 **단일 책임 원칙(Single Responsibility Rule, SRP)**을 깨뜨리기 때문에 이상적인 방법이 아닙니다.
다른 방법으로는, 검증 클래스를 만들어서 검증 역할을 위임하는 방법이 있습니다. 하지만, 이 경우에는 각각의 메서드 시작 부분에서 계속 검증 클래스를 호출해야 하는 단점이 있습니다.
검증 미들웨어를 만드는 것은 어떨까요? 동작은 하겠지만, 전체 어플리케이션의 모든 컨텍스트에서 사용할 수 있는 일반적인 미들웨어로 만드는 건 불가능할 겁니다. 이는 미들웨어가 핸들러나 매개변수에 대한 정보를 가진 실행 컨텍스트에 접근할 수 없기 때문입니다.
눈치채셨겠지만, 이것이 파이프가 만들어진 이유입니다. 그럼, 이제 검증 파이프를 개선하러 가봅시다!
Object schema validation
객체를 깔끔하고 DRY하게 검증하는 몇 개의 방법이 있습니다. 그 중 일반적인 방법은, 스키마 기반 검증을 사용하는 것입니다. 이 방법을 사용해보겠습니다.
Zod 라이브러리를 사용하면 읽기 쉬운 API를 사용하여 간단한 방식으로 스키마를 만들 수 있습니다. Zod 기반 스키마를 사용하는 유효성 검사 파이프를 구축해 보겠습니다.
먼저, 필요한 패키지를 설치합니다.
$ npm install --save zod
아래의 예시 코드에서는, constructor
의 인수로 스키마를 가져와서 schema.parse()
메서드로 주어진 스키마에 대해 들어온 인수를 검증합니다.
위에서 말했듯이, 검증 파이프는 변하지 않은 값을 반환하거나, 예외를 발생시킵니다.
다음 섹션에서는, 어떻게 @UsePipes()
데코레이터로 주어진 컨트롤러 메서드의 적절한 스키마를 가져오는지 알아볼 것입니다. 이를 이용하면, 모든 컨텍스트에서 검증 파이프를 재사용할 수 있게 됩니다.
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}
Binding validation pips
앞서 변환 파이프를 바인딩하는 방법(예: ParseIntPipe 및 나머지 Parse* 파이프)을 살펴봤습니다. 유효성 검사 파이프를 바인딩하는 방법도 매우 간단합니다. 이 경우 메서드 호출 수준에서 파이프를 바인딩하고자 합니다. 현재 예제에서는 ZodValidationPipe를 사용하기 위해 다음을 수행해야 합니다:
- ZodValidationPipe의 인스턴스 생성
- 파이프의 클래스 생성자에 컨텍스트별 Zod 스키마를 전달
- 파이프를 메서드에 적용시킵니다.
Zod 스키마의 예제입니다.
import { z } from 'zod';
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;
@UsePipes()
데코레이터로 다음과 같이 사용할 수 있습니다.
// cats.controller.ts
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
주의:
zod
라이브러리는 tsconfig에서strictNullCheck
속성을 필요로 합니다.
Class validator
주의:
이 섹션에서 소개하는 기술은 타입스크립트가 필요합니다. 만약 어플리케이션이 바닐라 자바스크립트로 작성되었다면 아래 기술은 사용할 수 없습니다.
앞서 소개한 검증 기술의 대체제를 살펴보겠습니다.
Nest는 class-validator과 잘 작동합니다. 해당 라이브러리를 사용하면, 데코레이터 기반 검증을 할 수 있게 됩니다. 데코레이터 기반 검증은, 처리되는 속성의 메타타입에 접근할 수 있는 Nest의 파이프와 맞물려 동작할 때 매우 유용합니다. 시작하기 전에, 아래의 명령어를 실행하여 필요한 패키지를 설치해야합니다.
$ npm i --save class-validator class-transformer
설치하면, CreateCatDto
클래스에 몇 개의 데코레이터를 추가할 수 있게 됩니다. 이것이 이 기술의 중요한 장점인데, 따로 검증 클래스를 만들 필요 없이 CreateCatDto
클래스 하나만으로 POST 요청의 바디를 관리할 수 있게 됩니다.
// create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
힌트:
class-validator
의 다른 데코레이터를 살펴보려면 여기를 참고하세요.
이제, 위의 데코레이터들을 활용하는 ValidationPipe
클래스를 만들 수 있게 되었습니다.
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
힌트:
다시 한 번 말씀드리자면, ValidationPipe는 Nest에서 기본으로 제공되므로 일반적인 유효성 검사 파이프를 직접 구축할 필요가 없습니다. 기본 제공 ValidationPipe는 이 장에서 빌드한 샘플보다 더 많은 옵션을 제공하지만, 사용자 정의 파이프의 메커니즘을 설명하기 위해 기본으로 유지했습니다. 여기에서 많은 예제와 함께 자세한 내용을 확인할 수 있습니다.
알림:
위의 예시에서 class-transformer 라이브러리를 사용했습니다. 해당 라이브러리는 class-validator 라이브러리와 같은 사람이 만들었기 때문에, 두 라이브러리는 서로 잘 맞물려서 동작합니다.
이제 위의 코드를 잘 살펴보겠습니다. 먼저, transform()
메서드에 async
가 붙어있는 걸 주목해주세요. Nest는 동기 파이프와 비동기 파이프 둘 다 지원하기 때문에, 위와 같이 async
를 사용한 비동기 파이프도 만들 수 있습니다. 해당 메서드를 비동기로 만든 이유는, 몇 개의 class-validator을 통한 검증이 비동기로 처리될 수 있기 때문입니다.
다음으로 주목해야 하는 부분은, 구조 분해(Destructuring)를 사용해서 ArgumentMetadata
의 멤버 중 metatype 필드를 metatype
매개변수로 추출했다는 점입니다. 이는 ArgumentMetadata
를 완전히 가져와서, 추가적으로 메타타입을 할당하는 과정을 줄여서 간단하게 쓴 것입니다.
그 다음, toValidate()
헬퍼 함수를 봐주세요. 이는, 현재 처리되는 변수가 네이티브 자바스크립트 타입일 때, 검증 과정을 넘겨버리는 역할을 합니다. 이들은 검증 데코레이터를 붙일 수 없으므로, 굳이 검증 과정을 거칠 필요가 없으므로 넘기는 것입니다.
또, 검증 과정을 거치기 위해선 기본 자바스크립트 객체를 타입이 있는 객체(typed object)로 변환해야 합니다. 이를 위해 class-transformer
라이브러리의 plainToClass()
함수를 사용하였습니다. 이렇게 하는 이유는, 네트워크 요청으로부터 바디를 역직렬화 할 때, 들어오는 POST 바디 객체에 대한 타입 정보가 없기 때문입니다. 하지만 class-validator
를 통해 검증하려면 앞서 DTO에서 정의한 검증 데코레이터가 필요합니다. 즉, 들어오는 바디를 기본 바닐라 객체가 아닌 적절한 데코레이터가 붙어있는(decorated) 객체로 처리할 수 있도록 변환해주어야 바디 객체를 검증할 수 있다는 것입니다.
마지막으로, 앞서 언급했듯이 검증 파이프는 변하지 않은 값을 반환하거나 예외를 발생시킵니다.
이제, ValidationPipe
를 적용시켜 봅시다. 파이프는 매개변수 수준, 메서드 수준, 컨트롤러 수준, 전역 수준에 적용시킬 수 있습니다. 앞서 나왔던 Joi 기반 검증 파이프는 메서드 수준에 파이프를 적용한 예시였습니다. 아래의 예시에서는, 파이프 인스턴스를 라우트 핸들러의 @Body()
데코레이터에 적용하여 POST 바디를 검증하도록 만들었습니다.
// cats.controller.ts
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
매개변수 수준의 파이프는 특정 매개변수에 검증 로직을 적용할 때 유용합니다.
Global scoped pipes
ValidationPipe
는 최대한 범용적으로 만들어졌기 때문에 전체 애플리케이션의 모든 경로 핸들러에 적용되도록 전역 범위 파이프로 설정하면 그 유용성을 최대한 발휘할 수 있습니다.
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
참고:
하이브리드 어플리케이션의 경우,useGlobalPipes()
로는 게이트웨이와 마이크로서비스에 파이프를 등록할 수 없습니다. 대신, 하이브리드가 아닌 "표준" 마이크로서비스 어플리케이션에는useGlobalPipes()
로 전역 파이프를 등록할 수 있습니다.
전역 파이프는 전체 어플리케이션의 모든 컨트롤러, 모든 라우트 핸들러에 적용됩니다.
위의 예시처럼 모듈 밖에서 useGlobalPipes()
로 전역 파이프를 등록하면, 적용이 이미 모듈 외부의 컨텍스트에서 끝났기 때문에 의존성 주입이 불가능합니다. 이 문제를 해결하려면, 전역 파이프를 아래처럼 모듈에 직접 설정하면 됩니다.
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
힌트:
파이프가 의존성 주입이 되도록 위와 같이 만들면, 어떤 모듈에서 설정했던 파이프는 전역이 됩니다. 따라서, 전역 파이프를 따로 선언하는 모듈을 따로 두시는 게 좋습니다. 또한,useClass
는 사용자 지정 프로바이더를 등록하는 유일한 방법이 아닙니다. 자세한 건 여기를 참고하세요.
빌트인 ValidationPipe
위에서 언급했듯이, Nest가 ValidationPipe
를 기본적으로 제공하기 때문에, 굳이 일반적인 검증 파이프를 직접 구현할 필요는 없습니다. 빌트인 ValidationPipe
는 이번 챕터에서 만든 샘플보다 더 많은 옵션를 제공하며, 만들어본 샘플은 그저 사용자 지정 파이프의 매커니즘을 설명하기 위해 만든 것일 뿐입니다. 자세한 건 여기를 참고해주세요.
변형 파이프 사용 예시
검증 파이프만이 커스텀 파이프의 사용 예시인 것은 아닙니다. 이 챕터를 시작할 때, 파이프는 입력 데이터를 원하는 형태로 변형할 수도 있다고 언급했습니다. 이는 transform
함수의 반환 값이 이전의 인수 값을 완전하게 덮어쓰기 때문에 가능합니다.
이건 언제 유용할까요? 클라이언트에서 전달된 데이터가 라우트 핸들러 메서드로 들어가기 전에, 문자열을 정수로 변환하는 등의 변형이 필요한 경우를 생각해보세요! 아니면 어떤 필수 필드의 데이터가 들어오지 않았을 때 기본값을 적용하고 싶을 때도 있을 것입니다. 변형 파이프는 클라이언트 요청과 요청 핸들러 사이에 끼어들어서 이러한 기능들을 수행합니다.
여기, 문자열을 정수값으로 변환하는 간단한 ParseIntPipe
가 있습니다. 물론 위에서 보았듯이 Nest는 더 정교한 빌트인 ParseIntPipe
를 갖고 있습니다. 아래의 파이프는 그저 커스텀 변형 파이프의 간단한 예시를 보여주기 위함입니다.
// parse-int.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
이제, 아래와 같이 매개변수에 변형 파이프를 적용해봅시다.
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
다른 변형 파이프의 유용한 예시는, 요청으로 들어온 아이디를 통해 데이터베이스에서 기존 유저 엔티티를 가져오는 것입니다.
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
UserByIdPipe
의 구현은 독자의 몫으로 남겨두겠습니다. 이 파이프도 다른 모든 변형 파이프들과 마찬가지로, 입력값(id
)을 받아서 출력값(UserEntity
)을 반환한다는 것만 기억하시면 됩니다. 이는 보일러플레이터 코드를 핸들러에서 꺼내서 일반적인 파이프로 만듦으로써, 코드를 더 선언적이고 DRY하게 만들 수 있습니다.
기본값 설정
Parse*
파이프들은 null
이나 undefined
값을 받으면 예외를 발생시기 때문에, 해당 파이프들을 사용하려면, 매개변수의 값이 정의되어 있어야 합니다. 만약 쿼리스트링 매개변수의 값이 없더라도 정상적으로 처리되게 하려면, Parse*
파이프가 해당 값들을 처리하기 전에 주입할 기본 값을 제공해야 합니다. 이를 위한 것이 바로 DefaultValuePipe
입니다. 다음과 같이 관련 있는 Parse*
파이프 앞에 DefaultValuePipe
를 두면 됩니다.
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}