1)

$ npm i @nestjs/mongoose mongoose
...
$ npm i --save @nestjs/config

app.module.ts

import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
import * as mongoose from 'mongoose';

@Module({
  imports: [
    ConfigModule.forRoot(),
    MongooseModule.forRoot(process.env.MONGODB),
    CatsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  private readonly isDev: boolean = process.env.MODE === 'dev' ? true : false;
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('cats');
    mongoose.set('debug', this.isDev);
  }
}

mongoose module을 설치하고 안전한 db 사용을 위해서 환경변수 module도 설치해주었다. debug 세팅을 하면 mongoose query도 찍어준다고 한다. set()은 mongooseOptions에서 가져왔다.

ConfigModule과 MongooseModule에서 forRoot() method를 사용하길래 타고 들어가 보니, ModuleMetadata interace를 확장한 DynamicModule을 반환한다고 한다. ModuleMetadata라고 하면 어렵게 들릴 수 있는데, @Module 장식자의 interface로 imports/controller/provider/exports를 property로 갖는 interface라고 생각하면 될 것 같다.

정적 모듈과 동적 모듈은 간단하게 말하자면 imports시 module class만을 가져올 것이냐, 아니면 register()나 forRoot() 같은 class의 method도 가져올 것이냐에서 차이가 있다. module을 가져오면서 instance화 된 service를 가져오는데(이에 따라 service간에 의존성 주입이 가능해질 것으로 이해했다.), 이 때 dynamic module은 변수를 이용한 customizing을 가능하게 한다.

https://velog.io/@pk3669/Nest에서-다이나믹-모듈Dynamic-module-개념만-알아보기
https://docs.nestjs.com/fundamentals/dynamic-modules
참조

2)

cats.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { IsEmail, IsString, IsNotEmpty } from 'class-validator';
import { Document, SchemaOptions } from 'mongoose';

const options: SchemaOptions = {
  timestamps: true,
};

@Schema()
export class Cat extends Document {
  @Prop({
    required: true,
    unique: true,
  })
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  @Prop({
    required: true,
  })
  name: string;

  @IsString()
  @IsNotEmpty()
  @Prop({
    required: true,
  })
  password: string;

  @Prop()
  imgUrl: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);
$ npm i --save class-validator class-transformer

main.ts

import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());

Document는 _id와 같은 collection에 삽입 될 document의 기본 사항들이 들어간다. Cat같은 경우는 일반적인 class이므로 SchemaFactory class를 거쳐야 비로소 CatSchema로 사용될 수 있다. createForClass method의 반환값이 mongoose.Schema이다.

class-validator를 설치하고 main.ts에 등록한 뒤 장식자 validation을 적용하면 type뿐만 아니라 정규표현식을 이용한 class-validator를 사용할 수 있다. 내부적으로 validate() method를 사용해서 주어진 object와 validationSchema를 비교하는 것으로 보인다.

https://github.com/typestack/class-validator 참조

3)

cats.request.dto.ts

import { IsEmail, IsString, IsNotEmpty } from 'class-validator';

export class CatRequestDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}

controller에서 body를 추출해서 넘겨줄 때, 그리고 해당 데이터가 service/db까지 타고 내려갈 때 validation check를 위해서 이전에 설치한 class-validator를 사용할 수 있다.

class란 이름이 붙은 것처럼, (상속 가능한)class 내부에서 장식자 validator를 사용하므로 재사용성이 높다고 할 수 있다.

4)

cats.module.ts

@Module({
  imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})

cats.service.ts

@Injectable()
export class CatsService {
    constructor(@InjectModel(Cat.name) private readonly catModel: Model<Cat>) {}
}

기존 Express 프로젝트에서 구축한 serivce/model/schema 구조가 Nest에서 service-module-schma 구조로 변형되면서 오르내림 구조가 module을 중심으로 분배/작동되는 인상을 받는다.

model이 작동하려면 먼저 module.ts에서 (dynamic module를 사용해서) 특정 schema가 customizing 되도록 작업해준다.

그런 다음에라야 service.ts에서 @InjectModel 장식자를 생성자에 넣어서(dependenct injection) model을 생성할 수 있다.

forFeature() method는 특정 schema를 가진 특정 name의 model을 반환하는데 Express에서처럼 (단지 db에 CRUD에 접근하려는 목적이 큰) model을 위한 폴더를 따로 할당하지 않아도 돼서 좋았다.

5)

cats.schema.ts

@Schema()
export class Cat extends Document {
...
  readonly readOnlyData: {
    id: string;
    email: string;
    name: string;
  };
}

export const CatSchema = SchemaFactory.createForClass(Cat);

CatSchema.virtual('readOnlyData').get(function (this: Cat) {
  return {
    id: this.id,
    email: this.email,
    name: this.name,
  };
});

cats.service.ts

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private readonly catModel: Model<Cat>) {}
...
    const hashedPassword = await bcrypt.hash(password, 10);
    const cat = await this.catModel.create({
      email,
      name,
      password: hashedPassword,
    });

    return cat.readOnlyData;
  }

service에서 반환되는 값이 모두 필요한 것은 아니다. 이럴 때 db가 아닌 서버에서 가상으로 생성한 object 형태를 제시하는 method가 virtual()이다. 이를 hydratedData라고도 한다.

https://stackoverflow.com/questions/6991135/what-does-it-mean-to-hydrate-an-object 참조

6)

$ npm install --save @nestjs/swagger

main.ts

import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
...
  const config = new DocumentBuilder()
    .setTitle('Cats example')
    .setDescription('The cats API description')
    .setVersion('1.0.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(process.env.PORT);
}
bootstrap();

cats.controller.ts

import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ReadOnlyCatDto } from './dto/cat.dto';

@Controller('cats')
@UseInterceptors(SuccessInterceptor)
@UseFilters(HttpExceptionFilter)
export class CatsController {
  constructor(private readonly catsService: CatsService) {}
...
  @ApiOperation({ summary: '회원가입을 위한 API입니다.' })
  @ApiResponse({
    status: 500,
    description: '서버 에러 발생 시',
  })
  @ApiResponse({
    status: 200,
    description: 'api 정상 동작',
    type: ReadOnlyCatDto,
  })
  @Post()
  async singUp(@Body() body: CatRequestDto) {
    return await this.catsService.signup(body);
  }

cats.request.dto.ts

import { ApiProperty, PickType } from '@nestjs/swagger';
import { IsEmail, IsString, IsNotEmpty } from 'class-validator';
import { Cat } from '../cats.schema';

export class CatRequestDto extends PickType(Cat, [
  'email',
  'name',
  'password',
] as const) {}

cat.dto.ts

import { ApiProperty, PickType } from '@nestjs/swagger';
import { Cat } from '../cats.schema';

export class ReadOnlyCatDto extends PickType(Cat, ['email', 'name'] as const) {
  @ApiProperty({
    example: '321321',
    description: 'id',
    required: true,
  })
  id: string;
}

cats.schema.ts

@Schema()
export class Cat extends Document {
  @ApiProperty({
    example: 'wild@wild.com',
    description: 'email',
    required: true,
  })
  @Prop({
    required: true,
    unique: true,

API설명을 용이하게 할 목적으로 swagger를 설치했다. 먼저 진입로인 main.ts에 'docs'라는 endpoint로 모듈을 설정해주었다.

어떤 API'가 요청/응답 되는지, 그리고 '풀어 쓰자면 어떤 API인지' 세 가지를 장식자로 등록했는데, 각각 ApiProperty(request.dto), ApiPerporty(cat.dto 생성)+ApiResponse, ApiOperation가 담당한다.

ApiRequest의 경우 body의 형태를 CatRequestDto class로 명시해줘서 따로 등록하지 않은 것 같고 ApiResponse는 기존에 따르던 형태가 없으므로 type 지정을 해주는데 이를 위해서 cat.dto.ts 파일을 생성했다. 같은 endpoint라도 status따라 달리 등록할 수 있다는 점이 재미있다.

schema 파일에 ApiProperty를 추가하고 이를 상속해서 재사용성을 높였다.

7)

cats.repository.ts

import { HttpException, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Cat } from './cats.schema';
import { CatRequestDto } from './dto/cats.request.dto';

@Injectable()
export class CatsRepository {
  constructor(@InjectModel(Cat.name) private readonly catModel: Model<Cat>) {}

  async existsByEmail(email: string) {
    const result = await this.catModel.exists({ email });
    return result;
  }

  async create(cat: CatRequestDto): Promise<Cat> {
    return await this.catModel.create(cat);
  }
}

cats.module.ts

@Module({
  imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
  controllers: [CatsController],
  providers: [CatsService, CatsRepository],
  exports: [CatsService],
})

cats.service.ts

@Injectable()
export class CatsService {
  constructor(private readonly catsRepository: CatsRepository) {}

  async signup(body: CatRequestDto) {
    const { email, name, password } = body;
    const isCatExist = await this.catsRepository.existsByEmail(email);
    if (isCatExist) {
      throw new UnauthorizedException('같은 email의 고양이가 이미 있습니다.');
    }

Express 프로젝트를 하면서 단순한 CRUD를 구현하는 model층을 두는 목적이 무엇인지 궁금했는데 repository 패턴이라는 대답을 얻을 수 있었다. 기본적으로 service-logic과 db 사이에 놓이는 buffer-layer 정도로 생각하면 좋을 것 같다.

repository 층위에서 구현된 class가 module의 provider로 등록되고('의존성 보증'이라고 불러도 좋지 않을까?) service에서 의존성 주입이 발생한다.

model은 그것이 구현된 파일에서 따로 신경쓰고 service-logic level에서는 input/output만 독립적으로 신경쓰면 돼서 명료하고 무엇보다 다른 종류의 db를 추가하거나 할 때, 작업이 보다 편리하다고 한다.

0개의 댓글