기억의 실마리
2023. 4. 7. 17:28

OpenAPI

openAPI는 애플리케이션의 REST-API 문서를 자동으로 구성해주는 라이브러리이다.

이 라이브러리를 사용하게된다면 개발을 진행할때 backend에서 API를 구성할때

지정한 타입이나 데이터 변수를 따로 문서화하여 frontend개발자에게 전달할 필요가

없어지기 때문에 능률을 올려주고 backend, frontend개발자 모두에게 편의를

제공할 수 있는 라이브러리이다.

 

(* React, NestJS로 구성된 프로젝트를 기준으로 포스팅하였다.)

 

Swagger? or OpenAPI?

Swagger는 2010년대 초 Tam Wordnik가 개발해온 라이브러리이다.
시작은 Wordnik기업에서 자체 API용 UI로 개발되었고 2015년초에SmartBear라는 회사에서 Swagger를 인수했다. 이 후에 2015년 말SmartBear는 Linux Foundation의 후원으로 OpenAPI Initiative에 Swagger를기부하면서 OpenAPI Specification으로 이름이 변경되었다.

지금의 Swagger는 OpenAPI를 Implement하기 위한 도구를 뜻하며

명세된API를 공유하는 swagger-ui이며 web에서 공유하는 ui-tool이다.

 

결과적으로 라이브러리 자체는 openAPI라고 명칭하며 swagger는 tool을 명칭한다.

 

 

swagger & openAPI Install

// express를 사용하는 경우
npm install --save nest-openapi-tools @nestjs/swagger swagger-ui-express

// fastify를 사용하는 경우
npm install --save nest-openapi-tools @nestjs/swagger swagger-ui-fastify

단순 명세기능을 사용할땐 swagger설치만 해도 명세가 가능하지만 필자는

codegen에 대해서도 다루기 때문에 nest-openapi-tools 를 함께 install 해주어

편의성을 더했다.

 

 

사용예시

 

1. DTO와 Controller 명세하기

// DTO or Entity
export class Dto {
  @ApiProperty({ type: Number }) // default타입(string)이 아니므로 타입지정
  id: number;

  @ApiProperty() // default타입 = string
  email: string;

  @ApiProperty({ required: false })
  password?: string;  // chaning = 필수요소x

  @ApiProperty()
  name: string;

  @ApiProperty()
  mobile: string;
}

// Controller
export class Controller {
  @ApiResponse({ type: CoreOutput }) // return 타입을 지정
  @Post('register')
  createAccount(@Body() input: CreateAccountDto): Promise<CoreOutput> {
    return this.userService.createAccount(input);
  }

  /** 유저데이터 수정 */
  @ApiResponse({ type: CoreOutput }) // return 타입을 지정
  @Patch('modify')
  @UseGuards(JwtAuthGuard)
  profileUpdate(
    @Req() req,
    @Body() updateData: UpdateAccountDto,
  ): Promise<CoreOutput> {
    return this.userService.profileUpdate(req.user, updateData);
  }
}

단순하게 데코레이터를 넣어줌으로서 명세가 가능하다.

 

 

2.  Front에 API명세파일 보내기

main.ts에 적용해도 되지만 main.ts에서 설정해야하는 것들이 많아지고

복잡해질 수 있기 때문에 openApi.ts를 만들어서 main.ts에 import시켜서 사용했다.

 

  • openApi.ts
import { OpenApiNestFactory } from 'nest-openapi-tools';
import { DocumentBuilder } from '@nestjs/swagger';

export const useOpenApi = (app) => {
  return OpenApiNestFactory.configure(
    app,
    new DocumentBuilder()
      .setTitle('My API')
      .setDescription('An API to do awesome things')
      .addBearerAuth(),
    {
      // swagger-ui 설정 (web-ui)
      webServerOptions: {
        enabled: process.env.NODE_ENV !== 'production',
        path: 'api-docs',
      },

      // 자동생성된 api문서 경로 및 설정
      fileGeneratorOptions: {
        enabled: process.env.NODE_ENV !== 'production',
        outputFilePath: './openapi.yaml', // or ./openapi.json
      },

      // 서버에 저장된 api문서를 프론트로 보내주는 설정
      clientGeneratorOptions: {
        enabled: process.env.NODE_ENV !== 'production',
        type: 'typescript-axios', //typescript-axios
        outputFolderPath: '../frontend/src/openapi',
        additionalProperties:
          'apiPackage=apis,modelPackage=models,withoutPrefixEnums=true,withSeparateModelsAndApi=true',
        openApiFilePath: './openapi.yaml', // or ./openapi.json
        skipValidation: true, // optional, false by default
      },
    },
    {
      // 명세함수의 기본값을 설정해준다.
      operationIdFactory: (c: string, method: string) => method,
    },
  );
};

 

  • main.ts
function bootstrap() {
  (async () => {
    try {
      const app = await NestFactory.create(AppModule);

      await useOpenApi(app);
      
      // ...
}
bootstrap();

 

사용팁

서버를 돌리고 있을때 위의 설정들이 제대로 갖춰질 경우 해당 경로에

openapi.yaml 의 API명세파일이 생길 것이고 서버가 수정될 때마다

openapi.yaml를 최신화 시킨다. 그렇기 때문에 계속해서 최신화때문에

서버로딩이 길어지게 되니 개발중엔 enabled 설정을 'production'일때로 바꿔주고

최신화시켜주고 싶을때 다시 설정을 'production'가 아닐때로 바꿔주면 된다.

 

 

3.  Front에서 서버의 API명세파일 가져오기

Front에서는 package.json에서 script설정을 통해 npm 명령어를 사용하여

명세파일을 가져오거나 최신화 시켜 줄 수 있다.

이를 위해서는 Front에 openapi-generator-cli 를 install해야한다.

 

  • openapi-generator-cli Install
// 패키지가 많은경우 충돌방지목적으로 -D 옵션을 사용하면 좋다.
npm install @openapitools/openapi-generator-cli -D

 

  • package.json
{
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "codegen": "npx @openapitools/openapi-generator-cli generate -i ../backend/openapi.yaml -g typescript-axios --additional-properties apiPackage=apis,modelPackage=models,withoutPrefixEnums=true,withSeparateModelsAndApi=true -o ./src/openapi"
  },
}

 

codegen script의 설명

  • "npx @openapitools/openapi-generator-cli"는 OpenAPI Generator CLI를 실행하기 위한 명령어
  • generate는 생성 명령을 나타내는 옵션
  • -i ../backend/openapi.yaml는 가져오는 API명세파일의 위치와 이름
  • -g typescript-axios는 TypeScript Axios Front에서 통신할 함수를 생성하기 위한 Generator 모듈을 지정
  • --additional-properties apiPackage=apis,
    modelPackage=models,
    withoutPrefixEnums=true,
    withSeparateModelsAndApi=true
    코드 생성에 사용되는 추가 속성 설정이다. 이 예제에서는 생성된 코드의 패키지 이름을 지정하고, Enum의 접두사를 제거한 후, 모델 및 API 코드를 분리하는 옵션이다.
  • -o ./src/openapi"는 생성된 파일의 위치를 지정하고 이 예제에서는 src/openapi 폴더에 생성된 파일을 저장한다.

 

명령어로 가져오기

// 터미널에 입력
npm run codegen

 Thanks for using OpenAPI Generator. 

문구가 뜨면 코드젠이 완료된 상태이다.

 

마치며...

작성일자에 진행중인 개인프로젝트에서 openAPI-codegen을 처음 사용해보았는데 typeORM을 사용했을때 만큼의 신선한 충격이었다. codegen은 효율을 혁신적으로 올려주는 라이브러리임을 확실하게 깨달았다. 실무에 들어가게 됐을때 openAPI-codegen과 swagger를 사용하지 않는 배경이라면 API명세문서의 간편화를 주장하며 이를 채용하자고 적극적으로 행동해야겠다는 생각이 들었다.

'Backend > Nest.js' 카테고리의 다른 글

[ Nest.js ] Node.js기반의 API 프레임워크  (0) 2023.01.24
2023. 1. 24. 15:14

Nest.js

Nest.js는 Node.js기반의 웹  API 프레임워크로서 Express, Fastify 프레임워크를

래핑하여 동작한다. 기본적으로는 Exress를 제공한다.

 

Nest.js의 장점

Node.js는 사용하기에 있어 편리하고 뛰어난 확장성을 가지고 있다.
이러한 특성으로 SW의 품질이 일정하지않고 라이브러리를 찾기위해
사용자가 많은 시간을 할애해야 한다. 반면 Nest.js는 데이터베이스,
ORM, Configuration, 유효성 검사 등 수많은 기능을 기본으로 제공하고 있다.
그러면서도 필요한 라이브러리를 쉽게 설치가능하기 떄문에

Node.js의 장점도그대로 가지고 있다.

 

 

Nest.js의 특징

Angular로부터 영향을 받아 모듈,컴포넌트 기반으로 프로그램을 작성함으로서

재사용성을 높인다. IoC(Inversion of Control), DI(Dependency Injection),

AOP(Aspect Oriented Programming)와 같은 객체지향 개념을 도입했다.

언어는 타입스크립트를 기본으로 채택하고 있다.

 

사용예시

Nest.js Install

npm i -g @nestjs/cli
//인스톨이 잘 되었는지 터미널에 nest를 입력해서 확인해본다.

인스톨이 잘 된 경우 터미널에 nest를 입력시 nest의 리스트가 나오게 된다.

 

nest new Project
//Project이름을 가진 Default create-nest가 생성된다.

프로젝트를 새로 만들어야 할 경우 Nest.js의 createt기능을 사용하여

Default프로젝트를 만들 수 있다.

 

 

src 폴더 구조

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      //transform은 요청데이터를 필요한 데이터형태로 바꿔준다. 원래는 string으로 받지만 number로 바꿔주고 있다.
    }),
  );
  await app.listen(3000); //run start시에 사용되는 서버포트이다.
}
bootstrap();

 

app.modules.ts

import { Module } from '@nestjs/common';
import { MoviesModule } from './movies/movies.module';
import { AppController } from './app.controller';

@Module({
  imports: [MoviesModule],
  controllers: [AppController],
  providers: [],
})
export class AppModule {}

 

app.controller.ts

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

@Controller('')
export class AppController {
  @Get()
  home() {
    return 'Welcome to my Movie API';
  }
}

src폴더는 Nest.js에서 root가 되는 구조이며 service.ts파일은 현재 사용예시 구조상 필요하지않고

movies폴더 내부에서 실질적인 구현을 대신하고 있으므로  movies폴더 내부에 service.ts파일이 있다.

 

 

movies 폴더구조

dto

dto는 코드를 간결하게 만들어 줄 수 있고 클라이언트로 부터 request를 받을때

유효성 검사를 해주기 위함이다.

 

create-movie.dto.ts

import { IsNumber, IsOptional, IsString } from 'class-validator';

export class CreateMovieDTO {
  @IsString()
  readonly title: string;

  @IsNumber()
  readonly year: number;

  @IsOptional()
  @IsString({ each: true })
  readonly genres: string[];
}
//DTO를 만드는이유는 코드를 간결하게 해주고 NestJS가 들어오는 쿼리에 대해 유효성을 검사할 수 있게 도와준다.

 

update-movie.dto.ts

import { PartialType } from '@nestjs/mapped-types';
import { CreateMovieDTO } from './create-movie.dto';

export class UpdateMovieDTO extends PartialType(CreateMovieDTO) {}

 

entities

entities는 DataBase에 넣어 줄 스키마를 미리 지정해주는 역할을 한다.

현재 예시는 간단하게 클래스를 지정하여 만들어 주었지만 실제 서비스에서는

typeORM을 사용하여 직접적으로 DB에 스키마를 만들어 줄 수 있으며

이 외에도 Prisma, Sequelize ORM 등이 있다.

movie.entity.ts

export class Movie {
  id: number;
  title: string;
  year: number;
  genres: string[];
}

 

movies 내부

movies.controller.ts

import {
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Patch,
  Body,
} from '@nestjs/common';
import { MoviesService } from './movies.service';
import { Movie } from './entities/movie.entity';
import { CreateMovieDTO } from './dto/create-movie.dto';
import { UpdateMovieDTO } from './dto/update-movie.dto';

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}
  /* movies.module.ts에서 @Module을 사용해서 controllers와 providers에서 import시켜주었기 때문에
  constructor(private readonly moviesService: MoviesService)와 같이 타입을 추가하는 것만으로
  같이 사용할 수 있게 되었다. 이것을 dependency injection이라고 한다.
   */

  @Get()
  getAll(): Movie[] {
    return this.moviesService.getAll();
  }

  @Get(':id')
  getOne(@Param('id') movieId: number): Movie {
    //필요한 것이 있을땐 반드시 요청을 해야한다. @Param()을 통해서 요청을 하고 props로 받는 구조다.
    console.log(typeof movieId);
    return this.moviesService.getOne(movieId);
  }

  @Post()
  create(@Body() movieData: CreateMovieDTO) {
    //@Body를 사용해서 클라이언트에서 보낸 object형식의 JSON을 추가할 수 있다.
    return this.moviesService.create(movieData);
  }

  @Delete(':id')
  remove(@Param('id') movieId: number) {
    return this.moviesService.deleteOne(movieId);
  }

  @Patch(':id') //데코레이터 Put과 Patch두가지가 있는데 Put은 모든 리소스를 받아오기 때문에 적합하지 않을 수 있다.
  patch(@Param('id') movieId: number, @Body() updateData: UpdateMovieDTO) {
    return this.moviesService.update(movieId, updateData);
  }

 

movies.module.ts

import { Module } from '@nestjs/common';
import { MoviesController } from './movies.controller';
import { MoviesService } from './movies.service';

@Module({
  controllers: [MoviesController],
  providers: [MoviesService],
})
export class MoviesModule {}

 

movies.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './entities/movie.entity';
import { CreateMovieDTO } from './dto/create-movie.dto';
import { UpdateMovieDTO } from './dto/update-movie.dto';

@Injectable()
export class MoviesService {
  private movies: Movie[] = [];

  getAll(): Movie[] {
    return this.movies;
  }
  //가짜 데이터베이스이며, 진짜데이터베이스에서는 Query를 가져온다.

  getOne(id: number): Movie {
    const movie = this.movies.find((movie) => movie.id === id);
    if (!movie) {
      throw new NotFoundException(`Movie with ID: ${id} not found.`);
      //HttpException에서 확장된 NestJS의 제공기능. 예외처리 기능을 한다.
    }
    return movie;
  }

  deleteOne(id: number) {
    this.getOne(id);
    this.movies = this.movies.filter((movie) => movie.id !== id);
  }

  create(movieData: CreateMovieDTO) {
    this.movies.push({
      id: this.movies.length + 1,
      ...movieData,
    });
  }

  update(id: number, updateData: UpdateMovieDTO) {
    const movie = this.getOne(id);
    this.deleteOne(id);
    this.movies.push({ ...movie, ...updateData });
  }
}

service.ts에서의 역할은 주로 서버에서 필요한 function의 집합체라고 이해할 수 있다.

가장 상위에서 @Injectable 데코레이터를 통해서 module내부에 provider로서

기능들을 주입할 수 있다.

 

 

마치며...

아키텍쳐를 정해두고 블록처럼 쌓아가는 형식으로 만들 수 있는것은 협업을 했을땐 오히려 가독성이 좋고 오차를 줄일 수 있는 장점이 있을 것 같다. 또한 프로그래밍의 방식을 고민해야할 것들이 이미 가이드라인으로 잡혀있다는 것 자체도 협업을 하는 시점에서 오해의 여지가 줄어들어 작업 효율이 좋아질 것 같아 보였다.

 

https://github.com/zeriong/Nest_intro