📜

NestJS、PrismaとSwaggerでREST APIを開発するチュートリアル

2023/07/09に公開

はじめに

今回の記事では、NestJSやPrismaを用いて、簡単なCRUD機能付きのREST APIを開発する手順を解説する。今回の記事では、『ドラゴンクエスト』に登場する武器のデータを表示するREST APIを開発する。

対象読者

  • これからREST APIの設計・開発でNestJSを使いたいひと
  • NestJSやPrismaに興味のあるひと

開発環境

  • Node.js 20.4.0
  • NestJS 9.4.0
  • Prisma 4.15.0
  • Visual Studio Code 1.79
  • SQLite
  • Windows 11

本記事で使う技術の説明

Node.js

JavaScriptやTypeScriptのWeb開発で必要不可欠な、JavaScript実行環境。

SQLite

環境構築要らずで、簡単にSQLのデータベースを構築できるRDBMS(リレーショナルデータベース)。多種多様なプログラミング言語に対応している。ハンズオンでデータベースを扱う上で最適。

NestJS

Prisma

環境構築

まずは、NestJSとPrismaでREST APIを開発するために必要なツールをインストールする。

(1) Node.js、NestJSをインストールする

Node.jsの公式サイトからNode.jsをインストールする。その後、ターミナルあるいはコマンドプロンプトを開いて、以下のコマンドでNestJSをインストールする。

npm i -g @nestjs/cli

(2) NestJSのプロジェクトを作成する

以下のコマンドを入力する。

npx nest new sample-api

(3) Prisma CLIをインストールする

cdコマンドでプロジェクトフォルダに移動し、Prisma CLIをインストールする。

cd sample-api
npm i prisma --save-dev

Prismaの設定

(4) Prismaの初期化

以下のコマンドを入力する。

npx prisma init

上述の操作でprismaディレクトリが生成され、schema.prismaファイルがその中に作成される。

(5) schema.prismaの作成

スキーマを設計するにあたって、『ドラゴンクエスト』に登場する武器のデータを箇条書きで確認する。

  • ID(id)
  • 名前(name)
  • 武器の攻撃力(attackPower)
  • 武器の特性(attribute)

以下のようにschema.prismaファイルを書く。

(6) データベースのマイグレーション

まずは、以下のコマンドでPrisma Clientをインストールする。

npm i @prisma/client

(5)までの手順で、データベースと接続するためのPrismaの設定が完了した。Weaponモデルを作成し、これがデータベース内のWeaponテーブルを作成する。このモデルを利用して、データベースを以下のコマンドでマイグレートする。

npx prisma migrate dev --name init

これでPrismaの設定が完了する。

REST APIの実装

ここから、実際にNestJSでREST APIを実装する手順を解説する。

まずは、以下のコマンドでModule、ControllerやServiceを作成する。

# Module
npx nest g mo weapons

# Controller
npx nest g co weapons

# Service
npx nest g s weapons

(7) Service

ServiceはNestJSにおける中核的な概念で、具体的なビジネスロジックを含むクラスだ。データベースへのクエリの実行、エラーハンドリング、データのバリデーションや変換、外部APIへのリクエストなど、実際の作業の大部分はServiceが担当する。

Serviceは一般的にControllerから呼び出され、その結果をControllerに返す。これにより、ControllerはHTTPリクエストとレスポンスのハンドリングに集中でき、ビジネスロジックはServiceに委ねられる。Serviceはテストしやすく、再利用可能であるべきである。

より簡潔に言えば、下記のServiceのコードはPrismaを利用してSQLiteデータベースに対する操作を提供している

上述のコード(src/weapons/weapons.service.ts)は非常に長いので、細かく再分割してそれぞれ詳細に解説する。

getAllWeapons()関数は、すべての武器データをSQLiteデータベースから取得している。PrismaのfindMany()メソッドを使う。

import { Injectable } from '@nestjs/common';
import { PrismaClient, Weapon } from '@prisma/client';

@Injectable()
export class WeaponsService {
  private prisma: PrismaClient;
  
  // インスタンスを直接作成
  constructor() {
    this.prisma = new PrismaClient();
  }

  async getAllWeapons(): Promise<Weapon[]> {
    return this.prisma.weapon.findMany();
  }
}

getWeapon(id: number)関数は、指定したIDの武器のデータをデータベースから取得する。PrismaのfindUnique()メソッドを使って、検索条件をwhereオプションで指定している。

// src/weapons/weapons.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient, Weapon } from '@prisma/client';

@Injectable()
export class WeaponsService {
  private prisma: PrismaClient;

  async getWeapon(id: number): Promise<Weapon> {
    return this.prisma.weapon.findUnique({ where: { id } });
  }
}

ここでPromise<Weapon>asyncの部分について疑問を持ったひとは少なくないだろう。後述のcreateWeaponupdateWeapondeleteWeaponについても同様に説明できるので、ここで詳細に解説する。

  • async:非同期関数を宣言するためのキーワード。非同期関数は、常にPromiseを返す。このPromiseは、関数が結果を返すか(resolve)、エラーをスローするか(reject)のいずれかになるまで待機するという意味になる。
  • Promise<Weapon>:これは関数がWeapon型の値を持つPromiseを返す。つまり、関数が終了するときにはWeapon型の値が利用可能になる、ということだ。<Weapon>はジェネリックスで、TypeScriptにおける型を引数として扱う文法仕様である。

上述のコードでは、Prismaの各メソッド(findManyfindUniqueなど)は非同期処理を行う。これらのメソッドを呼び出すと、すぐに結果を返すのではなく、Promiseを返します。そして、そのPromiseはデータベースからのレスポンスを待つ間、処理をブロックすることなく他の処理を進め、レスポンスが返ってきたらresolve(またはエラーが発生したらreject)します。

asyncPromiseを使うことで、NestJS(TypeScript)は非同期的にデータベースからのレスポンスを待ちつつ、その間に他のタスク(他のリクエストのハンドリング、イベントの処理など)を実行できる。これによりパフォーマンスが向上し、全体的な応答時間を短縮できる

import { Injectable } from '@nestjs/common';
import { PrismaClient, Weapon } from '@prisma/client';

@Injectable()
export class WeaponsService {

  async createWeapon(data: { name: string; attackPower: number; attribute: string }): Promise<Weapon> {
    return this.prisma.weapon.create({ data });
  }

}

こちらのcreateWeapon()関数では、新しい武器のデータをデータベースに作成する。Prismaのcreate()メソッドを用いて作成するデータをdataオプションで指定している。

import { Injectable } from '@nestjs/common';
import { PrismaClient, Weapon } from '@prisma/client';

@Injectable()
export class WeaponsService {

  async updateWeapon(id: number, data: { name?: string; attackPower?: number; attribute?: string }): Promise<Weapon> {
    return this.prisma.weapon.update({ where: { id }, data });
  }

}

updateWeapon()関数では、指定したIDの武器データを更新する。Prismaのupdate()メソッドを使って、更新するデータとそのIDをそれぞれdatawhereオプションで指定している。

import { Injectable } from '@nestjs/common';
import { PrismaClient, Weapon } from '@prisma/client';

@Injectable()
export class WeaponsService {

  async deleteWeapon(id: number): Promise<Weapon> {
    return this.prisma.weapon.delete({ where: { id } });
  }

}

deleteWeapon()関数では、指定したIDの武器のデータをデータベースから削除する。Prismaのdelete()メソッドを使用し、削除するデータのIDをwhereオプションで指定している。

(8) Controller

ControllerはNestJSアプリケーションにおけるエントリーポイントで、クライアントからのHTTPリクエストを受け取り、適切なレスポンスを返す。各Controllerは一つまたは複数のルートにマッピングされ、そのルートへのリクエストが来たときに特定のアクション(関数)を実行する。

Controllerの主な役割はHTTPリクエストのハンドリングとレスポンスの送信である。ビジネスロジック自体はServiceに委ねる。これにより、Controllerはシンプルでテストしやすく、その役割が明確になる。

上述のソースコードは長いので、再分割してポイントを簡潔に解説する。

import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { WeaponsService } from './weapons.service';

// APIのURLのパスが`weapons`で始まることを意味する
@Controller('weapons')
export class WeaponsController {

  // これは、Controllerクラスの各メソッドからweaponsServiceの各メソッドにアクセスできることを意味する。
  constructor(private readonly weaponsService: WeaponsService) {}

}

@Controller('weapons')デコレータは、クラスをControllerとして登録する。引数に指定した文字列、ここでは'weapons'はこのControllerでハンドリングされるルートへのパスを定義する。言いかえれば、このControllerのすべてのエンドポイントは/weaponsパスで始まることを意味する。

続いて、HTTPリクエストを行っているソースコードを確認しよう。

@Controller('weapons')
export class WeaponsController {

  // GETメソッド
  @Get()
  getAllWeapons() {
    return this.weaponsService.getAllWeapons();
  }

  // GETメソッド。特定のデータを抽出する
  @Get(':id')
  getWeapon(@Param('id') id: string) {
    return this.weaponsService.getWeapon(+id);
  }

  // POSTメソッド。データを新規作成する
  @Post()
  createWeapon(@Body() weaponData: { name: string; attackPower: number; attribute: string }) {
    return this.weaponsService.createWeapon(weaponData);
  }

  // PUTメソッド。データを上書きする
  @Put(':id')
  updateWeapon(@Param('id') id: string, @Body() weaponData: { name?: string; attackPower?: number; attribute?: string }) {
    return this.weaponsService.updateWeapon(+id, weaponData);
  }

  // DELETEメソッド。データを削除する
  @Delete(':id')
  deleteWeapon(@Param('id') id: string) {
    return this.weaponsService.deleteWeapon(+id);
  }
}

上述の、weaponDataの部分が冗長なので、これをコンパクトに書く方法を後述で紹介する。

@Post()
createWeapon(@Body() weaponData: { name: string; attackPower: number; attribute: string }) {
return this.weaponsService.createWeapon(weaponData);
}

@Put(':id')
updateWeapon(@Param('id') id: string, @Body() weaponData: { name?: string; attackPower?: number; attribute?: string }) {
return this.weaponsService.updateWeapon(+id, weaponData);
}

上述のソースコードに、ドラゴンクエストの武器データのAPIに関する情報や定義を書いていく。

(9) SwaggerでAPIドキュメンテーションを作成

Swaggerは、REST APIを設計、構築、文書化、消費するためのオープンソースのソフトウェアフレームワークの一つである。主な利点は以下の通りだ。

  • APIの設計と検証: Swagger Editorを使用すると、APIの設計とその正確性をリアルタイムで確認できる。
  • APIのドキュメンテーション: Swagger UIは、人間が読める形式でAPIのドキュメンテーションを生成します。これにより、エンドユーザーや他の開発者がAPIを理解しやすくなる。
  • APIのテスト: Swagger UIはまた、ブラウザから直接APIエンドポイントに対してリクエストを送信し、レスポンスを確認することができるインタラクティブな探索ツールも提供している。

NestJSでは、Swaggerを導入すると、エンドポイントの定義、リクエストとレスポンスの構造、認証方法などを自動的にドキュメンテーション化できる。これにより、APIの開発と保守が簡単になるのだ。NestJSは@nestjs/swaggerパッケージを提供している。これを使用すると、アプリケーションのエンドポイントやデータモデルにデコレータを付けるだけで、Swaggerの設定とドキュメンテーションの生成を簡単にできる。

具体的には、NestJSアプリケーションのmain.tsファイルでSwaggerモジュールをインポートし、設定する。次に、ControllerやデータモデルにSwaggerのデコレータ(@ApiProperty()など)を追加する。これにより、APIの各部分に関する詳細な情報がSwagger UIで表示され、そのAPIの利用方法を視覚的に理解できる。

これにより、開発者はAPIの開発中にAPIの動作をテストしたり、エンドユーザーや他の開発者にAPIの使い方を示したりすることが簡単になるのだ。

src/main.tsのコードで、特に注目するべき部分は以下。ポイントや挙動をコメント形式で解説した。

async function bootstrap() {
  
  // (1) アプリケーションのインスタンスを作成する
  // 引数に渡しているAppModuleはアプリケーションのルートモジュール
  const app = await NestFactory.create(AppModule);

  // (2) DocumentBuilderでSwaggerのAPIドキュメントの設定を行う
  const config = new DocumentBuilder()
    // タイトル設定
    .setTitle('DQ Weapons API')
    // APIドキュメントの概要を説明
    .setDescription('The DQ Weapons API description')
    // バージョンの表記
    .setVersion('1.0')
    // 設定の完了
    .build();
  
  // (3) SwaggerモジュールにAPIドキュメントを出力させる
  const document = SwaggerModule.createDocument(app, config);

  // (4) 生成したAPIドキュメンテーションを指定したパス(/api)に公開する
  // /apiにアクセスすると、Swagger UIを経由してAPIドキュメンテーションを閲覧できる
  SwaggerModule.setup('api', app, document);

  // (5) NestJSアプリケーションを指定したポートで起動する。ここでは3000
  await app.listen(3000);
}

上述のソースコードで、SwaggerのAPIドキュメントの情報を記述する。

(10) DTOクラス

Data Transfer Object(DTO) は、クライアントとサーバー間でデータをやり取りする際に使用されるオブジェクトのことを意味する。DTOは単にデータの形状を定義する役割を果たし、クライアントが送信できるデータの形状とタイプを明示的に示す。これにより、APIのエンドポイントが期待するリクエストボディの形状が明確になるのだ。

NestJSでは、DTOは通常クラスとして定義され、そのプロパティは装飾子(デコレータ)を用いて詳細に記述される。これらの装飾子は、データの型や必須項目、検証ルールなどを定義するために使われる。

まずは、データを作成する(create)ためのDTOを書く。

上述のCreateWeaponDtoクラスは、『ドラゴンクエスト』の新しい武器データを生成するためのDTOである。それぞれのプロパティは、Swaggerを使用してAPIドキュメンテーションを自動的に生成するために@ApiProperty()デコレータで装飾されている。

上述のDTOは、POSTメソッドで新しい武器データを作成する際に、リクエストボディの形状を定義するために使われる。Controllerでこれを使う場合は以下のように記述する。

// src/weapons/weapons.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateWeaponDto } from './dto/create-weapon.dto';
import { WeaponsService } from './weapons.service';

@Controller('weapons')
export class WeaponsController {
  constructor(private readonly weaponsService: WeaponsService) {}

  @Post()
  async create(@Body() createWeaponDto: CreateWeaponDto) {
    await this.weaponsService.create(createWeaponDto);
  }
}

@Body()デコレータを使用することで、リクエストボディが CreateWeaponDtoの形状に適合しているかがチェックされ、適合していない場合はエラーレスポンスが出力される。

上述のUpdateWeaponDtoクラスは、『ドラゴンクエスト』の武器データを更新するためのData Transfer Objectである。

上述のDTOは、PUTメソッドあるいはPATCHメソッドで武器データを更新する際に、リクエストボディの形状を定義するために使われる。例えば、Controllerでは以下のように記述する。

// src/weapons/weapons.controller.ts
import { Body, Controller, Patch, Param } from '@nestjs/common';
import { UpdateWeaponDto } from './dto/update-weapon.dto';
import { WeaponsService } from './weapons.service';

@Controller('weapons')
export class WeaponsController {
  constructor(private readonly weaponsService: WeaponsService) {}

  @Put(':id')
  async update(@Param('id') id: string, @Body() updateWeaponDto: UpdateWeaponDto) {
    await this.weaponsService.update(id, updateWeaponDto);
  }
}

上述の例では、@Patch(':id')デコレータが使われており、idパラメータで特定の武器を識別する。そして、その武器のデータをUpdateWeaponDtoに定義された形状のデータで更新する。このとき、リクエストボディがUpdateWeaponDtoの形状に適合していない場合、NestJSは自動的にエラーレスポンスを出力する。

コラム:DTOを導入するメリット

ここで、DTOの概念を導入すると以下のようにコードをコンパクトに書けるのだ。

ここで、(8) Controllerのパートを思い出してほしい。主にクライアントからのHTTPリクエストを受け取って、適切なレスポンスを返すsrc/weapons/weapons.controller.tsでは、データを送信するPOSTメソッドやPUTメソッドでは以下のように表記した。

// src/weapons/weapons.controller.ts
@Post()
createWeapon(@Body() weaponData: { name: string; attackPower: number; attribute: string }) {
  return this.weaponsService.createWeapon(weaponData);
}

@Put(':id')
updateWeapon(@Param('id') id: string, @Body() weaponData: { name?: string; attackPower?: number; attribute?: string }) {
  return this.weaponsService.updateWeapon(+id, weaponData);
}
// NOTE: コードを簡略化しているが、DTOを導入するなら以下のようにimport文を記述する必要がある
import { CreateWeaponDto } from './dto/create-weapon.dto';
import { UpdateWeaponDto } from './dto/update-weapon.dto';

@Post()
async create(@Body() createWeaponDto: CreateWeaponDto) {
  await this.weaponsService.create(createWeaponDto);
}

@Put(':id')
async update(@Param('id') id: string, @Body() updateWeaponDto: UpdateWeaponDto) {
  await this.weaponsService.update(id, updateWeaponDto);
}

このようにDTOはプロジェクトの依存関係を整理する上で便利である。NestJSでREST APIを開発する上では積極的に活用してほしい。

(11) ControllerにSwaggerやDTOの情報を与える

これらのSwaggerやDTOの情報を、Controllerにすべて与えると以下のように記述できる。

上述ソースコードでは、APIドキュメンテーションを自動生成するためのSwaggerの設定も行われている。@ApiTags('weapons')は、Swagger UIで表示されるタグを定義しており、これによりAPIエンドポイントがカテゴリー別に整理される。また、WeaponsControllerクラスはWeaponsServiceを注入しており、これにより各エンドポイントでは実際のデータ操作を行うサービスのメソッドが呼び出される。これにより、ControllerはHTTPリクエストのルーティングとバリデーションに集中でき、実際のデータの処理はServiceに委ねられる。

以上が、NestJS、Prisma、SQLite、Swaggerを使用して、CRUD機能を実装したREST APIを作成する手順になる。これはあくまで基本的な例であり、実際の開発ではエラーハンドリングや認証など、さらに多くの側面を考慮する必要がある。

ディレクトリ

/
├── package.json
├── prisma/
│   ├── .env
│   └── schema.prisma
├── src/
│   ├── app.module.ts
│   ├── main.ts
│   ├── weapons/
│   │   ├── dto/
│   │   │   ├── create-weapon.dto.ts
│   │   │   └── update-weapon.dto.ts
│   │   ├── weapons.controller.ts
│   │   ├── weapons.module.ts
│   │   └── weapons.service.ts
└── node_modules/

上述のディレクトリ内にある、それぞれのフォルダの説明を簡潔にする。

  • package.json: Node.jsのプロジェクト設定ファイル。使用するパッケージの情報や、プロジェクトの起動スクリプトなどを記述します。
  • prisma/: Prismaの設定ファイルやデータモデルのスキーマが含まれるディレクトリ。
  • src/: ソースコードを格納するディレクトリ。
  • app.module.ts: アプリケーションのルートモジュール。他のすべてのモジュールとサービスはこのモジュールからインポートまたはエクスポートされる。
  • main.ts: アプリケーションのエントリーポイント。ここからアプリケーションが起動する。
  • weapons/: 武器に関連するコントローラー、サービス、モジュール、DTOが格納されている。
  • node_modules/: プロジェクトで使用するNode.jsのパッケージが格納されているディレクトリ。

参考記事

https://www.prisma.io/blog/nestjs-prisma-rest-api-7D056s1BmOL0

https://medium.com/@teten.nugraha/building-a-rest-api-with-nestjs-and-prisma-orm-e52c8e182ae3

GitHubで編集を提案

Discussion