📝

NestJS の採用理由と使用感

こんにちは。

株式会社アルダグラムの安政です。

今回はアルダグラムで採用している NestJS についての所感をお伝えできればと思います。

NestJS とは

Node.js のフレームワークの一つであり、TypeScript 製のバックエンドフレームワークです。

https://nestjs.com/

コマンドが用意されているので、簡単にプロジェクトやテンプレートを導入できます。

また、拡張性・抽象化に優れており、NestJSの主要なアーキテクチャのカスタムコンポーネントを簡単に作ることができます。

ドキュメントも充実しており、想定されているレシピも多数紹介されているので、ミドルウェアとの接続や、アプリケーション設計なども簡単に行えます。

https://docs.nestjs.com/

アルダグラムでの採用理由

アルダグラムでは、フロントエンドの開発に Next.js / React Native を用いて開発を行っています。

どちらも TypeScript を用いた開発を行うので、その技術をいかしたサーバサイドのフレームワークとして採用しました。

アルダグラムのサーバサイド API として GraphQL を採用しているので、そことの親和性があるのも選択した理由の一つです。

NestJS で開発しているプロダクトでは必要な機能が軽く動くものというのも意識しています。

メインで利用しているレシピ

NestJS では複数のレシピやモジュールが公開されています。

アルダグラムでは、OpenAPI のモジュール構成を活用して開発を行っています。

https://docs.nestjs.com/openapi/introduction

デコレータや依存モジュールの指定で ルーティング含めた API が構築できるのは直感的で良いものだと感じます。

採用してよかった点

コーディングしていて便利だと感じた点を記載します。

開発コストの削減が容易

共通処理のカスタム設計が容易なので、一度ベースの処理さえ書いてしまえば、拡張していくのは容易な印象でした。

アルダグラムでは、ページネーションやファイルのアップロード、標準レスポンスなどを共通化することでコードの再利用を行っています。

以下のようなコードでカスタムデコレータは作成できます。

※ 長くなるので、本質ではない箇所の import は省略しています。

// 標準レスポンスのカスタムデコレータ

import { applyDecorators } from '@nestjs/common'
import {
  ApiBadRequestResponse,
  ApiForbiddenResponse,
  ApiNotFoundResponse,
  ApiUnauthorizedResponse,
  ApiInternalServerErrorResponse
} from '@nestjs/swagger'

import { ErrorResponse } from '@interfaces/error/error-response'

export function SummarizeApiResponse() {
  return applyDecorators(
    ApiBadRequestResponse({
      type: ErrorResponse,
      description: 'BadRequest.'
    }),
    ApiForbiddenResponse({
      type: ErrorResponse,
      description: 'Forbidden.'
    }),
    ApiUnauthorizedResponse({
      type: ErrorResponse,
      description: 'Unauthorized.'
    }),
    ApiNotFoundResponse({
      type: ErrorResponse,
      description: 'Not Found.'
    }),
    ApiInternalServerErrorResponse({
      type: ErrorResponse,
      description: 'Internal Server Error.'
    })
  )
}
// ページネーション定義のカスタムデコレータ

import { applyDecorators, HttpStatus, Type } from '@nestjs/common'
import { ApiResponse } from '@nestjs/swagger'

export function ApiResponseWithPagination(responseObject: Type<unknown>) {
  return applyDecorators(
    ApiResponse({
      status: HttpStatus.OK,
      type: responseObject,
      headers: {
        total_count: { description: '総ヒット件数' },
        total_page: { description: '全てのページ件数' },
        next_page: { description: '次のページ' },
        has_next_page: { description: '次のページが存在するか' }
      }
    })
  )
}

カスタムデコレータは以下のような形でコントローラに設定できます。

import { ApiResponseWithPagination } from '@core/decorators/api-response-with-pagination.decorator'
import { SummarizeApiResponse } from '@core/decorators/summarize-api-response.decorator'

@SummarizeApiResponse()
@Controller('samples')
@ApiTags('samples')
export class SamplesController {
  constructor(private readonly samplesService: SamplesService) {}

  @Get()
  @ApiOperation({ summary: 'サンプル一覧を取得する' })
  @ApiResponseWithPagination(GetSamplesResponse)
  async findAll(@Query() params: GetSamplesRequest, @Res() res: Response) {
    return this.samplesService.findAll({
      page: Number(params.page),
      limit: Number(params.limit)
    })
  }
}

バリデーションやスキーマの設定が簡単

DTO の定義ファイルを作成し、スキーマに対してプロパティを設定することで簡単に定義を行うことが可能です。

https://docs.nestjs.com/openapi/types-and-parameters

また、TypeScript 利用前提ですが、クラスバリデーターというライブラリと親和性があり、バリデーションの設定が容易かつ直感的に行えます。

https://docs.nestjs.com/pipes#class-validator

デコレータによるコーディングでソースコードが読みやすい

コントローラ内などで、ロジックと関係ない処理に関しては、デコレータによる記法で分離が行えます。

Swagger 内で表示する、スキーマ情報や API の説明などはデコレータとして定義することで、ソースコードのリーディングはスムーズに行える印象です。

スキーマ情報の定義例はこちらです。

class-validator を用いることでバリデーションの設定が容易にできます。

import { ApiProperty } from '@nestjs/swagger'
import { IsInt, Min, Max } from 'class-validator'
import { Type } from 'class-transformer'

export class PaginatedDto {
  @IsInt()
  @Min(1)
  @ApiProperty({
    default: 1
  })
  @Type(() => Number)
  page!: number

  @IsInt()
  @Min(1)
  @Max(100)
  @ApiProperty({
    minimum: 1,
    maximum: 100,
    default: 30
  })
  @Type(() => Number)
  limit!: number
}

実際には、 PaginatedDtoextends することで、必要な箇所のみに拡張しています。

こちらを @Query の型として登録することで、Swagger の定義やリクエストが叩かれた際のバリデーションに反映されます。

@Controller('samples')
@ApiTags('samples')
export class SamplesController {
  constructor(private readonly samplesService: SamplesService) {}

  @Get()
  @ApiOperation({ summary: 'サンプル一覧を取得する' })
  @ApiResponseWithPagination(GetSamplesResponse)
  async findAll(@Query() params: PaginatedDto, @Res() res: Response) {
    return this.samplesService.findAll({
      page: Number(params.page),
      limit: Number(params.limit)
    })
  }
}

また、 @ApiOperationsummary オプションを指定することで API の説明を加えることができています。

Swagger による API 定義書の自動出力が便利

コントローラで設定した定義から Swagger の WebUI を出力できるのはもちろんですが、定義書をファイルとして出力することもできます。

現状の運用では、外部に対して定義書自体を提供することもあり、機能を見つけた際にはとても安心できました。

const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())

const options = new DocumentBuilder()
  .setTitle('Sample')
  .setVersion('1.0')
  .build()

const document = SwaggerModule.createDocument(app, options)

// JSON
fs.writeFileSync(
  './swagger-spec.json',
  JSON.stringify(document, undefined, 2)
)

// YAML
fs.writeFileSync('./swagger-spec.yaml', dump(document, {}))

微妙だなと感じた点

利用していて微妙かなと感じた点を記載します。

最終的には「慣れ」により解消されますので、ご参考までにです。

学習コストが高い

モジュールの指定やコントローラに対するデコレータの定義方法など、通常の処理であっても、理解するまでに時間がかかる印象でした。

また、サーバサイド言語ですが、型依存の言語に触れていない方にとっては、ハードルは少し上がるかなと思います。

階層化を重ねると依存モジュールの指定がややこしい

Service 層でカスタムモジュールを利用したい場合、モジュール層で定義を行っている必要があります。

ロジックを書くソースまでの階層を深く設計すると、モジュールを辿る旅が始まります。

デコレータの記述を見逃すことが多い

一部にのみ、設定を行うデコレータなどがあると、設定されているかの確認を見逃すケースが多いです。

nest cli などから自動定義できるといいかと思いますが、今後の課題としています。

最後に

今回は NestJS に関する採用理由と所感についてお話ししました。

技術選定などを行う際のご参考になれば幸いです。

今後はサンプルコードを交えつつの、アプリケーション内での工夫や技術的に課題であった点などをご紹介できればと考えています。

アルダグラム Tech Blog

Discussion