웹 풀사이클 데브코스 TIL [Day 87] - NestJS 시작해보기

JaeKyung Hwang·2024년 3월 29일
0
post-thumbnail

🐱NestJS

Node.js server-side application

TypeScript로 만들어졌고 TypeScript를 완전히 지원한다!

  • 독립적인 프레임워크는 아니고 Express/Fastify와 같은 HTTP 서버 프레임워크 위에 얹어져서 일관된 API로 노출시켜주는 Adaptor 느낌쓰
  • 기존에 사용했던 Express의 경우 프로젝트 구조가 자유로운 프레임워크였기 때문에 구조화에 있어서 개인적으로 고민이 많았는데 Nest에서 이 부분을 보완해주는 것 같다.

📥설치 (with Nest CLI)

npm i -g @nestjs/cli
nest new project-name
  • TypeScript의 strict feature를 사용하고 싶으면 --strict flag를 사용해서 프로젝트 생성
    nest new project-name --strict
  • project-name이라는 이름의 기본적인 보일러플레이트 프로젝트가 생성된다.
    src
     ├── app.controller.spec.ts
     ├── app.controller.ts
     ├── app.module.ts
     ├── app.service.ts
     └── main.ts
  • 생성된 project는 linter(eslint)와 formatter(prettier)가 미리 설치되어 있다. (formatters vs linters)
    # Lint and autofix with eslint
    $ npm run lint
    
    # Format with prettier
    $ npm run format
  • formatter인 prettier를 저장할 때마다, 또는 붙여넣기 할 때마다 적용시키고 싶어서 나는 다음과 같이 설정해주었다.
    • Crtl + , > format 검색 (User, Workspace 모두 동일하게 세팅)
      • Default Formatter: Prettier - Code formatter 적용
      • Format On Save: 체크
      • Format On Paste: 체크
    • 생성된 .vscode/settings.json에 다음 내용 추가(저장해도 제대로 적용이 안되는 경우에만)
        "[javascript]": {
          "editor.defaultFormatter": "esbenp.prettier-vscode"
        },
        "[javascriptreact]": {
          "editor.defaultFormatter": "esbenp.prettier-vscode"
        },
        "[typescript]": {
          "editor.defaultFormatter": "esbenp.prettier-vscode"
        },
        "[typescriptreact]": {
          "editor.defaultFormatter": "esbenp.prettier-vscode"
        }
    • .prettierrc에서 원하는대로 설정
        {
            "singleQuote": true,
            "trailingComma": "all",
            "tabWidth": 2,
            "printWidth": 80,
            "endOfLine": "auto"
        }
  • prettier에서 Delete ␍ eslintprettier/prettier 오류 발생 시 .eslintrc.js의 rules field에 다음 내용 추가
    rules: {
      …,
      'prettier/prettier': [
        'error',
        {
          endOfLine: 'auto',
        },
      ],
    },

🏗️기본 구조

Controllers

들어오는 요청을 처리하고 클라이언트에 응답을 반환하는 역할

  • class와 decorator를 사용 → decorator는 class를 필수 metadata와 연결하고 Nest가 routing map을 생성할 수 있도록 한다.

💡 validation 기능이 내장된 CRUD controller를 CLI의 CRUD generator를 사용해 신속하게 생성 가능: nest g resource [name]

  • handler level로 routing
    • decorator(@Get()@Post()@Put()@Delete()@Patch()@Options()@Head()) 지원
  • Response를 조작하는 두 가지 방법
    1. Standard (recommended)
      • controller의 method(request handler)에서 그냥 값을 return하면 Nest가 알아서 처리해준다!
        • JavaScript object나 array를 반환하면 자동으로 JSON으로 serialize됨
        • JavaScript primitive type(e.g., stringnumberboolean)을 return하면 serialize하지 않고 값을 그냥 보냄
        • 응답의 status code는 default로 200 (POST는 201)이고 handler-level에서 @HttpCode(...)를 사용해 변경 가능
    2. Library-specific
      • express의 response object를 @Res() decorator를 사용하면 response.status(200).send()와 같이 이용 가능

        다만 Nest는 @Res()나 @Next()를 handler에서 감지하면 Nest의 Standard approach는 비활성화된다.

        따라서 특정 기능만 response object를 주입해 사용하고 나머지는 Nest framwork의 기능을 사용하도록 두고 싶다면 반드시 @Res({ passthrough: true })로 option을 설정해주어야 한다.

Providers

의존성 주입이 가능한 services, repositories, factories, helpers 등의 providers로 취급되는 basic Nest classes

  • @Injectable()이라는 decorator를 사용하여 다른 객체에 주입 가능하도록 설정
  • providers는 lifetime(”scope”)를 가짐
    • Provicer Scope
      1. DEFAULT: singleton providers의 lifecycle이 전체 application의 lifecycle과 동기화

      2. REQUEST: provider의 새로운 instance가 각각의 incoming request에 대해 생성되고 요청 처리가 완료된 후에는 instance가 garbage-collected됨

        import { Injectable, Scope } from '@nestjs/common';
        
        @Injectable({ scope: Scope.REQUEST })
        export class CatsService {}
      3. TRANSIENT: transient providers는 consumers 사이에 공유되지 않고, provider를 주입하는 각각의 consumer는 새로운 전용 instance를 받음

        import { Injectable, Scope } from '@nestjs/common';
        
        @Injectable({ scope: Scope.TRANSIENT })
        export class CacheManager {}
        // provider registration
        {
          provide: 'CACHE_MANAGER',
          useClass: CacheManager,
          scope: Scope.TRANSIENT,
        }

Modules

@Module() decorator로 annotate한 class

  • @Module() decorator는 다음 property들을 가지는 단일 객체를 인자로 받는다.

    providersNest injector에 의해 인스턴스화되고 적어도 이 module 전체에서 공유될 수 있는 providers
    controllers인스턴스화되어야 하는 이 module에 정의된 controllers
    imports이 module에 필요한 providers를 export하는 module들을 import하는 목록
    exports이 module에서 제공되는 providers의 하위 집합으로 다른 module에서 이 module을 import해서 사용 가능 (provider 자체나 해당하는 token(provide value)을 사용할 수 있음)
  • Nest에서 module은 default로 singleton ⇒ 같은 provider의 instance를 여러 module들에서 쉽게 공유 가능!!

    ex. CatsService라는 provider의 instance를 다른 module들에서 공유하기

    1. CatsService provider를 module의 exports array에 추가한다.

      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 {}
    2. 이제 CatsModule을 import하는 어떤 module에서든 CatsService에 접근 가능하며 같은 instance를 공유한다.

  • @Global() decorator를 사용하면 module이 global-scope가 되며 해당 module을 import array에 추가하지 않아도 export한 provider를 사용할 수 있다.

    import { Module, Global } from '@nestjs/common';
    import { CatsController } from './cats.controller';
    import { CatsService } from './cats.service';
    
    @Global()
    @Module({
      controllers: [CatsController],
      providers: [CatsService],
      exports: [CatsService],
    })
    export class CatsModule {}

Pipes

input data의 transformation이나 validation을 위해 사용 (변환 가능하면 변환 후 controller route handler로 넘어가고 변환이 실패하면 exception 발생)

  • Nest에서 기본적으로 사용 가능한 9가지 pipe
    • ValidationPipe
    • ParseIntPipe
    • ParseFloatPipe
    • ParseBoolPipe
    • ParseArrayPipe
    • ParseUUIDPipe
    • ParseEnumPipe
    • DefaultValuePipe
    • ParseFilePipe
  • Parse* pipe
    @Get(':id')
    async findOne(@Param('id', ParseIntPipe) id: number) {
      return this.catsService.findOne(id);
    }
    +) option을 사용할 수도 있음
    @Get(':id')
    async findOne(
      @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
      id: number,
    ) {
      return this.catsService.findOne(id);
    }
    GET localhost:3000/abc
    
    {
      "statusCode": 400,
      "message": "Validation failed (numeric string is expected)",
      "error": "Bad Request"
    }
  • class-validator를 사용하면 DTO를 정의한 후 ValidationPipe를 binding해서 손쉽게 validation이 가능하다.
    • create-cat.dto.ts
      import { IsString, IsInt } from 'class-validator';
      
      export class CreateCatDto {
        @IsString()
        name: string;
      
        @IsInt()
        age: number;
      
        @IsString()
        breed: string;
      }
    • cats.controller.ts
      @Post()
      async create(
        @Body(new ValidationPipe()) createCatDto: CreateCatDto,
      ) {
        this.catsService.create(createCatDto);
      }
  • main.tsapp.useGlobalPipes()를 이용해 global scoped pipe로 설정할 수 있다. 모든 route handler에 적용된다.
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.useGlobalPipes(new ValidationPipe());
      await app.listen(3000);
    }
    bootstrap();
profile
이것저것 관심 많은 개발자👩‍💻

0개의 댓글