nestjs, mysql, typeorm, swagger, logging, Errors, pagination, jwt, kafka정리

노요셉·2021년 3월 14일
2

준비: node 설치되어있음.
source: https://github.com/leaveittogod0123/blog_nest

nestjs 프레임워크 사용에 필요한 nestjs/cli

sudo npm i -g @nestjs/cli

nest 프레임워크기반 프로젝트 생성

nest new hello-nestjs

mysql, typeorm 패키지 설치

yarn add @nestjs/typeorm typeorm mysql2

ormconfig.json 생성

{
    "type": "mysql",
    "host": "localhost",
    "port": 3307,
    "username": "root",
    "password": "toor",
    "database": "DB",
    "entities": ["dist/**/*.entity{.ts,.js}"],
    "synchronize": true
  }

TypeOrmModule

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

@Module({
  imports: [TypeOrmModule.forRoot()], // <- TypeOrmModule 설정
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

typeorm 참고 : https://yangeok.github.io/orm/2020/12/14/typeorm-decorators.html

nest 프로젝트 실행

npm run start

nest 모듈 생성

nest g mo user

nest 만 쳐도 명령어 리스트가 뜸 또는 사이트에서 설명하고 같이 볼 수 있음
the usages of nestjs cli commands

entity 생성

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class UserEntity {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({unique: true})
  username: string;

}

interface 생성 dto 역할

export interface User {
    id?: string;
    name?: string;
    username?: string;
}

DTO validation (원하는 위치에)

nestjs의 ValidationPipe를 쓰면 전역범위 파이프가 설정됩니다.

기존로직에 잘돌아가는 Pipe가 적용되지 않은 로직도 pipe로직을 타게 됩니다.
원하는 위치에만 유효성 파이프가 동작하게 해봅니다.
https://velog.io/@peter0618/NestJS-request-body-validation

@UsePipe(new ValidationPipe())

userModule에 UserEntity 적용

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './user.entity';
import { UserService } from './user.service';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
  providers: [UserService]
})
export class UserModule {}

nest 서비스 생성

nest g s user

참고

만약에 monorepo로 프로젝트를 구성했으면

/Users/yosephnoh/dev/blog-nest
└── api
    ├── README.md
    ├── dist
    ├── docker-compose.yml
    ├── nest-cli.json
    ├── node_modules
    ├── ormconfig.json
    ├── package.json
    ├── src
    ├── test
    ├── tsconfig.build.json
    ├── tsconfig.json
    └── yarn.lock

.eslintrc 에 아래 코드 추가
eslint가 cannot read tsconfig.json 어쩌구할텐데 아래처럼 바꿔줘야함.

  parserOptions: {
    project: './tsconfig.json',
    sourceType: 'module',
    tsconfigRootDir: __dirname,
  },

https://stackoverflow.com/questions/63355118/typescript-eslint-tsconfig-resolution-error-in-monorepo

EntityManager

queryBuilder

https://github.com/typeorm/typeorm/blob/master/docs/select-query-builder.md#how-to-create-and-use-a-querybuilder

엔티티 매니저로 find, insert 하는 것보다 보통 join해서 select하는 경우가 많기 때문에 단일 table에서 데이터 가져오는거 빼고는 다 queryBuilder를 사용할 것

참고: https://github.com/typeorm/typeorm/issues/4725


configuration

https://docs.nestjs.com/techniques/configuration
환경변수를 통해 앱을 제어하고 싶을때 사용함.

애플리케이션들은 종종 다른 환경(dev, staging, prod)에서 동작해요. 환경에 따라 다른 설정이 사용되야해요. 예를 들어, 사용자가 사용하는 머신에서는 (로컬) 특정 DB credential(접속정보)가 보통 로컬 환경에 따르는데, 배포를 해서 cloud에 있는 개발머신 (개발서버) 에서는 또 다른 DB 접속정보를 갖게될 수 있잖아요. 이렇게 환경이 변함에 따라 환경변수를 저장해서, 사용하는 방법은 효율적입니다. node.js 애플리케이션에서는 process.env 를 통해 외부에서 선언한 환경변수를 참조해서 환경이 변함에 따라 다른 DB 접속 정보로 동작하게 할 수 있어요.

node.js 애플리케이션에서는 .env 파일을 사용해요. 그 파일 내부에는
key=value 형태의 값들이 있고, 애플리케이션에서 이 값들을 사용해서 다르게 동작하게 되요.

ConfigService

https://docs.nestjs.com/techniques/configuration#using-the-configservice

ConfigService를 사용하기 위해서는 ConfigModule을 import 받아야함.
configService를 사용하려는 module에 import 할것 ( AppModule에서 isGlobal로 사용하지 않았으면.. )

schema Validator를 쓰려면 Joi를 쓰면 됌.
https://docs.nestjs.com/techniques/configuration#schema-validation

보통 .env 파일에는 아래와 같이 쓰이지만

DATABASE_HOST=127.0.0.1
...

configService로 환경변수 참조할때는 대소문자 구분 안하는거 같음.

host: this.configService.get<string>('database.host'),

swagger

출처: https://www.youtube.com/watch?v=r0TP4DdXeIk

yarn add @nestjs/swagger swagger-ui-express

swagger setup

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const options = new DocumentBuilder()
    .setTitle('blog_nest API')
    .setDescription('blog_nest API')
    .setVersion('1.0.0.')
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

ApiProperty description ( DTO )

import { ApiProperty } from '@nestjs/swagger';

export class User {
  @ApiProperty({ description: '회원 ID' })
  id?: string;
  @ApiProperty({ description: '회원 닉넴' })
  name?: string;
  @ApiProperty({ description: '회원 이름' })
  username?: string;
  @ApiProperty({ description: '회원 이메일' })
  email?: string;
  @ApiProperty({ description: '회원 비밀번호' })
  password?: string;
}

ApiOkResponse description (Controller)

  @Get()
  @ApiOperation({ summary: '회원 목록 조회 API' })
  @ApiOkResponse({ type: [User] })
  findAll(): Observable<User[]> {
    return this.userService.findAll();
  }

@ApiUnauthorizedResponse({ description: 'Invalid crendentials' })

  @Post('login')
  @ApiOperation({ summary: '회원 로그인 API' })
  @ApiOkResponse({ description: 'User login' })
  @ApiUnauthorizedResponse({ description: 'Invalid crendentials' })
  login(@Body() user: User): Observable<any> {
    return this.userService.login(user).pipe(
      map((jwt: string) => {
        return { accessToken: jwt };
      }),
      catchError((err) => of({ error: err.message })),
    );
  }

Authorization 토큰 설정하기

https://www.youtube.com/watch?v=r0TP4DdXeIk
youtube보고 하다가 안되서 개고생했는데, stackoverflow에도 안나오고 nestjs 공식 문서 repo보고 해결
https://github.com/nestjs/nest/blob/master/sample/11-swagger/src/main.ts
단순히, addBearerAuth()만 추가해줌. youtube보고 option을 params로 넣었는데, option 사용법을 잘못했는지, Header에 Authrization이 전달되지 않고 요청갔음..

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AllExceptionsFilter } from './filters/exception/all-exception.filter';

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

  app.useGlobalFilters(new AllExceptionsFilter());

  const options = new DocumentBuilder()
    .setTitle('blog_nest API')
    .setDescription('blog_nest API')
    .setVersion('1.0.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);
  await app.listen(3000);
}
bootstrap();

그리고 적용하려는 controller method에도 데코레이터 추가해주면 됌

  @ApiBearerAuth()
  @hasRoles('Admin')
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Get()
  @ApiOperation({ summary: '회원 목록 조회 API' })
  @ApiOkResponse({ type: [User] })
  findAll(): Observable<User[] | any> {
    return this.userService
      .findAll()
      .pipe(catchError((err) => of({ error: err.message })));
  }

swagger에 authorize 버튼이 생겼을텐데 token만 넣으면 됌. Bearer도 쓸필요가 없음


Guard

https://docs.nestjs.com/guards

guard는 @Injectable 데코레이터로 쓰이는 클래스이며, CanActivate 인터페이스를 구현해야한다.

권한, 인증 등을 통해 request가 route handler에서 다룰 수 있는지 없는지 체크한다.

controller-scope에서 적용되며, @UseGuards(<GuardName>) 데코레이터를 붙여서 사용할 수 있다.

main.ts에서 GlobalGuard로 셋업할 수도 있음.

providers에 DI처럼 해당 module에 포함되어있는 모든 route에 적용할 수도 있음.

Reflector: helper class(@nestjs/core 패키지에서 적용됨)를 통해 roles들 가져올 수 있음.


JWT

Jwt를 통해 user 정보를 받아서 token을 생성하고
그 token을 통해 API 요청시 token이 유효한지, expire되었는지 확인해서 유효한 경우에만 api 동작하게끔 합니다.

yarn add @nestjs/passport passport-jwt passport
yarn add -D @types/passport

JwtStrategy에서도 configService를 통해 .env의 jwt_secret 값을 가져오는데, 이게 뭔 문제가 있는지 그냥 provide로 service바인딩해주면 configService가 없어서 에러가 났음.
그래서 useFactory를 통해 async configService를 바인딩해서 해결함.

import { forwardRef, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-guard';
import { JwtStrategy } from './jwt-strategy';
import { RolesGuard } from './roles.guard';
import { UserModule } from 'src/modules/user/user.module';

@Module({
  imports: [
    forwardRef(() => UserModule),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get('JWT_SECRET'),
        signOptions: {
          expiresIn: '3600s',
        },
      }),
    }),
  ],
  providers: [
    ConfigService,
    AuthService,
    JwtAuthGuard,
    // JwtStrategy,
    {
      provide: JwtStrategy,
      useFactory: () => async (configService: ConfigService) => ({}),
    },
    RolesGuard,
  ],
  exports: [AuthService],
})
export class AuthModule {}

JwTStrategy

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    return { user: payload.user };
  }
}

만약에 configService.get('JWT_SECRET')가 반환한 값이 undefined면? AppModule의 imports에 ConfigModule.forRoot()를 해주었는지 체크해볼 것

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './modules/user/user.module';
import { AuthModule } from './modules/auth/auth.module';
import * as ormConfig from '../ormconfig';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRoot(ormConfig[0]),
    AuthModule,
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

공식 홈페이지에서 찾아보면
https://docs.nestjs.com/techniques/configuration#getting-started
forRoot() 정적 메서드를 실행함으로써 환경변수(key:value형식)가 parsed되고, resolved된다네요.

https://github.com/typeorm/typeorm/issues/1890

참고: https://www.codemag.com/Article/2001081/Nest.js-Step-by-Step-Part-3-Users-and-Authentication

레퍼런스: https://levelup.gitconnected.com/secures-apis-with-jwt-token-c471d1622cbc

forwardRef

https://docs.nestjs.com/fundamentals/circular-dependency#moduleref-class-alternative

https://velog.io/@peter0618/NestJs-circular-dependency-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0

Errors and Logging

https://www.youtube.com/watch?v=XP_gONOksuM

https://docs.nestjs.com/exception-filters#binding-filters

Error핸들링은 기존에는 http Exception만 잡으려 했으나 RoleGuard같은 Error를 핸들링 하지 못하여, Exception 전체를 잡아서 핸들링 하게끔 수정

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';

/**
 * HTTP exception 을 받아서 처리합니다.
 * Client 에 전달할 예외처리를 표준화 할 수 있습니다.
 */
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger: Logger = new Logger(this.constructor.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    this.logger.debug(`${this.constructor.name}.catch() works`);
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AllExceptionsFilter } from './filters/exception/all-exception.filter';

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

  app.useGlobalFilters(new AllExceptionsFilter());

  const options = new DocumentBuilder()
    .setTitle('blog_nest API')
    .setDescription('blog_nest API')
    .setVersion('1.0.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);
  await app.listen(3000);
}
bootstrap();

pagination

cursor-based pagination을 적용해볼 것임.
클라이언트가 가져간 마지막 row의 순서상 다음 row들을 n개 요청/응답하게 구현

커서 기반 페이지네이션은 우리가 원하는 데이터가 '어떤 데이터의 다음'에 있다는 데에 집중합니다. 이 row 다음 10개 주세요.

커서 기반 페이지네이션을 위해서는 반드시 정렬 기준이 되는 필드 중 적어도 한개는 고유값이여야 함.

커스텀 커서 생성

typeorm에 어떻게 적용할 것인가?

SELECT id, title, price,
		CONCAT(LPAD(price, 10, '0'), LPAD(id, 10, '0')) as `cursor`
	FROM `products`
	HAVING `cursor` < '00000058000000000242'
	ORDER BY price DESC, id DESC
	LIMIT 5;

영어로 table column이 아닌 계산 값을 뭐라고 할까 막 검색하다가 걸린 키워드 select additional computed column

https://github.com/typeorm/typeorm/issues/296#issuecomment-464416330

역시나 나랑 같은 고민을 가진 사람들이 있었음.

@Column( 'varchar', { length: 200, nullable: false, unique: true } )
name: string;

// readonly url
protected url: String;
    
@AfterLoad()
getUrl() {
    this.url = 'https://domain.com' + this.name + '.jpg';
}

이렇게 풀이할 수 있음.

cursor based pagination은 pagination navigation 같은 기능 제공할 수 없는 구조 아닌가?
cursor based pagination은 entity에 afterload를 통해 computed column을 가질 수 있는데, 그러면 정렬 기준까지 동적으로 바꿀 순 없는건가?
왜냐하면 정렬기준이 id, 이름, role 순으로 오름차순이라고 할때 정렬 순서까지 바꿔주게 api를 만들어줄 수 있나?

response 형태
https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/

참고 : https://velog.io/@minsangk/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-Cursor-based-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

https://jsonapi.org/profiles/ethanresnick/cursor-pagination/

Interceptors

https://docs.nestjs.com/interceptors#aspect-interception

service 처럼 @Injectable() 데코레이터를 붙이고,
NestInterceptor 인터페이스를 구현하면 된다.

AOP 기술의 영향을 받은 기능들을 탑재하고 있음.

Interceptors로 가능한 것들

  • 실행 로직에 before/ after method 추가적으로 바인딩할 수 있음.
  • 함수의 반환값을 변형할 수 있음.
  • 함수의 익셉션을 변형할 수 있음.
  • HOC 같은 기능을 할 수 있음.
  • 특정 상황에서 메서드를 오버라이딩 할 수 있음.

Basics

모든 Interceptor는 intercept(ExecutionContext, CallHandler)를 구현해야합니다.

Execution context

guards랑 거의 비슷한 인스턴스.
ArgumentsHost의 상속을 받음.

rabbitmq

https://www.youtube.com/watch?v=52FUjMOtoeY&list=PLlameCF3cMEt_PfXxVRT8_VckugrPBI-G

https://www.youtube.com/watch?v=sD-EnmnRh4c

https://www.youtube.com/watch?v=47KZw6z7_wM

kafka

https://dev.to/rajeshkumarbehura/kafkajs-nestjs-with-typescript-simplified-example-35ep


Overview

Controller

당연히 HTTP request, response 처리에 책임이 있다.
routing 메커니즘이 어떤 요청을 어떤 컨트롤러가 처리할지 통제합니다.

Routing

@Controller 데코레이터를 통해 필수 metadata를 class와 연관지어서 Nest에게 전달하고, Nest는 metadata를 기반으로 routing Map을 생성합니다.

@Controller 데코레이터에 옵셔널하게 path 인자를 전달하면 route path에 접두사로 붙게 됩니다. @Get, @Post 같은 요청 메서드 데코레이터에 인자를 두면 중첩으로 path를 만들 수 있습니다.

예를 들어,

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get('info')
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

GET /cats/info 와 같이 route mapping을 생성할 수 있습니다.

Nest에서는 응답을 조작하기 위해 두 가지 다른 옵션을 사용합니다.

Standard: 내장된 메서드를 이용하면 request handler가 Javascript Object 또는 array를 반환하면 자동으로 JSON으로 직렬화 합니다. 그러나, string, number, boolean을 반환하면 Nest는 직렬화를 시도하지 않고 값만 보냅니다.
또한, 응답의 상태코드는 201을 사용하는 POST요청을 제외하고는 항상 기본 200입니다. Status code을 통해 변경할 수 있습니다.

라이브러리-특화: Express같은 특정 라이브러리의 종속적인 response object를 사용할 수 있음.

Request object

Handler는 client의 request의 상세정보에 접근할 필요가 있다. Nest는 request object로 접근을 제공합니다.
Nest가 제공해주는 데코레이터를 이용해서 왠만한 정보는 request에 간단하게 접근할 수 있음.

Resource

Route parameters

Modules

module은 @Module데코레이터로 어노테이션된 클래스입니다. @Module 데코레이터는 Nest가 application 구조를 구성하는데 사용할 metadata를 제공합니다.

@Module을 작성하면 Nest application에게 구성하려는 구조를 알리는 역할을 하는거네요.

각 애플리케이션 적어도 한개의 root module을 가집니다. root module은 Nest가 application graph를 빌드할때 사용하는 시작점입니다. (1)

모듈을 통해 애플리케이션 효과적으로 구성하려고 했답니다.

@Module providers, controllers, imports, exports라는 프로퍼티를 갖습니다.

providers: Nest Injector로 인스턴스화 될 것들, 이 모듈이 아닌 다른 모듈에서도 공유될 것들이 포함됩니다.
(모듈 내에서 사용하려는 객체를 말하는것 같습니다. 물론 모듈 밖에서도 공유될 수도 있고요.)

controllers: 이 모듈에서 인스턴스화될 controller의 집합 ( 컬렉션이라고 봐도 되죠)

imports: export된 provider(인스턴스)나 모듈에서 필수로 사용해야할 다른 곳에 선언된 모듈들을 나열합니다.

exports: providers의 subset이며 이 모듈에서 제공되고, 다른 모듈에서 import 되어 이용가능한 인스턴트를 말합니다.

module은 기본적으로 providers를 캡슐화합니다.
이 말은 현재 모듈이나 exported되어 import할 모듈에 없는 providers는 주입할 수 없다는 것!
그래서 public interface나 API는 모듈로 구성되어있음을 고려할 수 있습니다.

Feature Modules

왜 feature 모듈이라하는가?

cat.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

첨부된 코드를 보면, Controller, Service가 같은 application domain에 속합니다. feature modules는 이렇게 밀접하게 연관되어, 특정 기능에 관련된 코드들이 구성한대요. 이렇게 구성함으로써, SOLID 원칙에의해 개발되고 복잡성을 관리하는데 도움을 줍니다.

app.module.ts

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

위에 CatModule을 정의했어요. 이걸 AppModule (root module)에서 CatModule을 import 해주면 CatModule의 기능을 사용할 수 있게 됩니다.

Shared Modules

Nest에서는 기본적으로 module은 singleton입니다. 따라서 여러 module에서 어렵지 않게 같은 instance를 공유할 수 있습니다.

모든 모듈은 자동으로 공유됩니다. 일단 만들면 다른 모듈에의해 재사용될 수 있어요.

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

CatsService 인스턴스를 다른 모듈에서 쓰고싶으면, 먼저CatsService를 export합니다.
그리고 CatsService를 사용하고 싶은 모듈에 가서 Import CatsModule을 하면 CatsService에 대한 접근이 가능합니다.

Module re-exporting

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

위에 나온대로 module는 내부 provider(인스턴스)를 export할 수 있었는데, 거기다가 import 된 module을 다시 export할 수 있습니다.

commonModule이 import되고 export되는데, 이렇게 해서 여러 모듈을 한번에 import하게 할 수 있게 됩니다.

Dependency Injection

모듈은 provider를 주입할 수도 있습니다. (가능은 합니다. 대신 설정 목적으로..)
모듈 자체의 class들이 주입될 수 없어요.
circular dependency

Global modules

예를 들어, 거의 모든 모듈에서 import해야하는 모듈 집합이 있으면, Nest에서는 module 스코프안에 providers를 캡슐화해요. 모듈을 import하지않고서는 modules의 정의된 provider를 사용할 수 없어요. (Angular와 이부분이 달라요)

어디서든지 helpers, database connections, 등등 바로 사용가능하게 하고싶으면 module에 @Global() 데코레이터를 붙이세요.

글로벌로 만드는건 좋은 설계는 아닙니다. imports 문을 사용하는 것을 선호되어집니다.

Dynamic modules

동적으로 모듈을 등록하고 설정하냐 마냐를 정할 수 있습니다.
dynamic modules


설명:
(1) application graph는 내부 자료 구조이며, module, provider 관계, 의존성을 해결하는데 사용합니다.

Providers

Nest에서 service, repositories, factories, helpers 등등이 provider개념으로 묶을 수 있습니다.
provider 핵심은 의존성 주입이 된다는 것인데요. Nest에게 인스턴스를 연결하는 부분을 위임하는 겁니다. nest가 대신 의존관계를 생성해줍니다.

CatsController는 단순히 Http 요청을 핸들링하고, 복잡한 태스크는 providers에게 위임하는 구조가 보편적입니다. Providers는 일반 자바스크립트 클래스고요. module의 providers 옵션에 선언된 것들입니다.

Service를 예로 들면

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

클래스가 있고, @Injectable 데코레이터가 보입니다. @Injectable 데코레이터룰 붙여서 Nest Ioc Container에서 CatsService를 관리할 수 있게 해줍니다. CatsService를 사용하는 부분은

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

이렇게 생성자에서 의존성을 주입받은 인스턴스를 사용해서 작업을 할 수 있습니다.
물론 module의 providers에도 선언해야 합니다.

Dependency Injection

Angular 공식문서에 이 개념이 나오니 읽어볼 것을 권장합니다.

타입스크립트 덕분에 type으로 구분이 가능하여 의존성 관리가 쉬워졌습니다.

다음 코드로 Nest가 CatsService를 생성하고, 인스턴스를 catsService에 할당하게 됩니다. 일반적으로 singleton으로 구성되고, 이렇게 의존성이 해결되고, controller의 생성자로 전달됩니다.

constructor(private catsService: CatsService) {}

scopes

provider의 생애주기는 application의 생애주기와 같습니다. 애플리케이션이 초기화될때 모든 의존성이 해결되야하며, 그러므로 모든 provider이 인스턴스화 됩니다. 애플리케이션이 종료되면 providers들도 파괴됩니다. 그러나 request-scoped으로 provider의 생애주기를 만들 수도 있습니다.
Injection Scopes

custom providers

DI fundamentals

DI는 Ioc Container에게 Nest에서 사용하는 의존관계를 갖는 인스턴스화 하는 작업을 위임하는 겁니다.

위에 나온 CatsController, CatsService, CatsModule 작성해두면

Nest IoC Container가 CatsController를 인스턴스화 하고 CatsService의 의존성을 찾고, CatsService token으로 IoC Container에서 인스턴스를 찾습니다. module에 providers 등록한대로 token이 등록됩니다. singleton scope(기본)으로 되어있다면, CatsService 인스턴스를 생성하고 캐시하고, 반환합니다. 기존 인스턴스가 있으면 기존 인스턴스를 반환합니다.

Standard providers

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

providers에 선언한 내용을 길게 풀어쓰면 다음과 같고 보통은 짧은 방식으로 사용합니다.

providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
];

custom providers

values providers

import { CatsService } from './cats.service';

const mockCatsService = {
  /* mock implementation
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

위 예로 보면 CatsService token으로 mockCatsService mock 객체가 resolve(즉 인스턴스를 반환한다)할 겁니다. CatsService와 같은 인터페이스를 가진 리터럴 객체가 대체하게 됩니다. Typescript의 structural typing 형태가 같으면 객체 호환성을 가질 수 있다. 즉 객체 형태만 같다면 할당할수 있다.

non-class-based provider tokens

providers의 provide 키를 token으로 의존성을 생성자 기반 주입할 수 있다 했는데, 보통 클래스 이름을 두었다. 그런데, 때로는 DI token으로 스트링을 쓸 수도 있다.

import { connection } from './connection';

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

이러면 'CONNECTION' 이란 문자열 토큰으로 import한 외부 파일에서 object를 연결할 수 있어요.

문자열 대신에 enum이다 javascript symbol을 써도 되요.

CONNECTION은 이렇게 가져다 쓸 수 있어요.

@Injectable()
export class CatsRepository {
  constructor(@Inject('CONNECTION') connection: Connection) {}
}

constants.ts 같이 파일을 분리해서 토큰을 정의하는게 좋아요!

나머지 내용은 다음에..

Optional providers

you might have dependencies which do not necessarily have to be resolved.
이런 경우가 구체적으로 어떤 경우인지 모르겠음.

Property-based injection

생성자에서 주입받는건 외에 Property기반의 주입이 가능하다는데, 왜 쓰는지에 대한 이유는 이해가 안됐음. super() 호출이 문제가 될 수 있는건가? 왜 문제가 되는거지?

Provider registration

Nest가 DI할 수 있게 module에 providers에 service를 붙여줍니다.

Manual Instantiation

Module reference
Standalone application

Middleware

미들웨어는 라우트 핸들러 (controller)전에 호출되는 함수이다.

또한, 미들웨어는 request, response 객체에 접근할 수 있습니다.

nest middleware는 express middlewre와 동등한 것이다.

middleware로는 뭘 할 수 있나?

  • 코드 실행
  • request, response 객체 수정
  • request-response 사이클 종료
    .. 어디에 쓸 수 있을지 모르겠다.

NestMiddleware 인터페이스를 구현해야합니다.

DI

적용됌.

Applying middleware

@Module 데코레이터를 보면 middleware 자리가 없다.
configure 메서드를 이용해서 모듈 클래스에서 적용해줍니다.

Middleware consumer

헬퍼 클래스입니다.

Excluding routes

미들웨어에 적용안하게 path, method 지정이 가능

Functional middleware

함수로 middleware 구성할 수 있음.

multiple middleware

여러 미들웨어 구성 가능.

global middleware

전역 미들웨어 구성 가능.

Exception Filters

애플리케이션 전체 범위에서 모든 unhandled exceptions를 책임지는 exception layer가 nest에 내장되어 구성 된다. 애플리케이션 코드에서 exception을 애플리케이션 코드에서 처리하지 못한 exception들이 이 layer에서 잡힌다.

Custom exceptions

이렇게 custom exceptions을 쓸까요? Error을 쓸까요?

Exception Filter

위에서 말했던 내장된 exception filter가 있다고 했는데, 이걸 직접 통제할 수도 있다.
ExceptionFilter를 implementes 하는 것!

나머진 와닫는게 없다.

Catch everything

모든 unhandled exception을 잡기 위해 @Catch에 파라미터를 비워둔다.

Inheritance

에러처리는 그대로 ExceptionFilter가 하게 해두고
거기에 상속해서 코드를 추가하는 방법도 있음.
BaseExceptionFilter

profile
서로 아는 것들을 공유해요~

0개의 댓글