/sample-rest

REST API which made in NestJS.

Primary LanguageTypeScript

【NestJSハンズオン #1】NestJSで簡単なREST APIを作ろう

こちらのリポジトリは2022年8月27日(土)に福岡市赤煉瓦文化館で実施される「【NestJSハンズオン #1】NestJSで簡単なREST APIを作ろう」で使われるものである。

開発環境

  • Visual Studio Code 1.70
  • Windows 11
  • TypeScript 4.8
  • NestJS 9.1.1
  • Prisma 4.2.1
  • Swagger 3.0.3
  • SQLite 3.39.2

大まかな手順

  1. Prismaのセットアップ
  2. データベースのマイグレーション
  3. データベースのデータ作成、prismaサービスの作成
  4. Swaggerのセットアップ
  5. GETPOSTPUTDELETEメソッドの実装
  6. レスポンス型の設定

初期設定

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

# NestJS CLIの場合
npx nest new <project-name>

# Gitコマンドの場合。公式がテンプレートを用意してくれるのでそれを使う
git clone https://github.com/nestjs/typescript-starter.git project
cd project
npm install

開発者サーバの立ち上げ

npm run start

http://localhost:3000/にアクセスすれば、画面にHello Worldが出力されるはず。

簡単なディレクトリ紹介

NestJSの初期のディレクトリは以下の通り。本章ではsrcフォルダのファイルを中心に話す。

src
 -  app.controller.spec.ts
 -  app.controller.ts
 -  app.module.ts
 -  app.service.ts
 -  main.ts

app.controller.spec.ts

Controllerのユニットテストを行う際に使用するファイル。

import { Test, TestingModule } from '@nestjs/testing'
import { AppController } from './app.controller'
import { AppService } from './app.service'

describe('AppController', () => {
  let app: TestingModule

  beforeAll(async () => {
    app = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile()
  })

  describe('getHello', () => {
    it('should return "Hello World!"', () => {
      const appController = app.get<AppController>(AppController)
      expect(appController.getHello()).toBe('Hello World!')
    });
  });
});

app.controller.ts

単一のrouteを持つ基本的なController

import { Controller, Get } from '@nestjs/common'
import { AppService } from './app.service'

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello()
  }
}

app.module.ts

NestJSアプリケーションの基盤となるModuleを扱う。

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'

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

app.service.ts

単一のメソッドを持つ基本的なService

import { Injectable } from '@nestjs/common'

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!'
  }
}

main.ts

コア関数NestFactoryを活用してNestJSアプリケーションのインスタンスを作成するアプリケーションのエントリーファイル。

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

// この処理で開発者サーバを起動できる。
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  await app.listen(3000)
}
bootstrap()

Prismaのセットアップ

PrismaはNode.jsとTypeScript専用のオープンソースのORMである。ORM(Object-Relational Mapping)はデータベースに対するデータの操作をオブジェクト指向型言語のやり方で扱えるようにするための手法である。

ORMを使うことで、データベースを設計する際にSQL言語を書く必要はなくなる。

最初に以下のコマンドを入力してPrismaをインストールする。

npm install prisma --save-dev

以下はPrisma CLIを使う。ここで、Prisma CLIのinitコマンドでPrismaの初期設定を行う。

npx prisma init

こちらのコマンドは、以下のファイルで新しいprismaディレクトリを作成する。

  • schema.prisma:データベース接続を指定し、データベーススキーマを格納する
  • .env:データベースの認証情報を環境変数のグループに格納する際に使う

データベースの設計

データベースへの接続はschema.prismaファイルのdatasourceブロックで設定される。デフォルトではpostgresqlに設定されているが、本チュートリアルではSQLiteを使うので、datasourceブロックのプロバイダフィールドをsqliteに調整しなければならない。

datasource db {
  provider = "sqlite" // ここを"postgresql" => "sqlite"にする
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

ここで、.envを開いてDATABASE_URL環境変数を以下のように調整する。

DATABASE_URL="file:./dev.db"

SQLiteは単純なファイルであり、SQLiteを使用するためにサーバーは必要ない。したがって、ホストとポートを含む接続URLを設定する代わりに、ローカルファイル(この場合dev.dbと呼ばれる)を指定するだけでいい。

モデルを設計する

実際に、schema.prismaファイルにデータベースの情報を書いていく。

datasource db {
  provider = "sqlite" // ここを"postgresql" => "sqlite"にする
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

// 以下のプログラムを追加する
model User {
  id Int @id @default(autoincrement()) // idを設定して、自動で生成する
  name String @unique // nameが他のnameと一致している場合はnull
  description String? //後ろに?をつけることで、nullableを示す。(要はなくてもいい)
}

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

schema.prismaファイルを作成した後は、以下のコマンドを入力する。

npx prisma migrate dev --name "init"

成功した場合、このようにターミナルにメッセージが表示される

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20220528101323_init/
    └─ migration.sql

Your database is now in sync with your schema.
...
✔ Generated Prisma Client (3.14.0 | library) to ./node_modules/@prisma/client in 31ms

以下のファイルはコマンドで生成されたSQLファイルになる。

-- CreateTable
CREATE TABLE "User" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "name" TEXT NOT NULL,
    "description" TEXT
);

-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");

データベースの作成

prismaディレクトリに新規でseed.tsファイルを作成する

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
    const post1 = await prisma.user.upsert({
        where: {name: 'Shota Nukumizu'}, // データベースを設置する場所
        update: {}, // データ更新をする必要がないのでとりあえず保留
        // データの中身を設計する
        create: {
            name: 'Shota Nukumizu',
            description: 'A programmer'
        },
    })

    const post2 = await prisma.user.upsert({
        where: {name: 'Furukawa Shuntaro'},
        update: {},
        create: {
            name: 'Furukawa Shuntaro',
            description: 'The President of Nintendo'
        }
    })

    console.log({post1, post2})
}

main()
    .catch((error) => {
        console.log(error)
        process.exit(1)
    })
    .finally(async () => {
        await prisma.$disconnect()
    })

package.jsonにprismaコマンドを入力する

// package.json

// ...
  "scripts": {
    // ...
  },
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
  },
  "jest": {
    // ...
  },
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }

seedコマンドは、以前に定義したprisma/seed.tsファイルを実行する。ts-nodeはすでにpackage.jsonでdev依存としてインストールされているため、このコマンドは自動的に動作する。

以下のコマンドでseedを実行する。

npx prisma db seed

Prismaサービスの作成

以下のコマンドで、NestJSアプリ開発に必要なModuleServiceをインストールしておく

npx nest g module prisma
npx nest g service prisma

これによって、新しいサブディレクトリ./src/prismaが生成され、prisma.module.tsprisma.service.tsファイルが生成される。

src/prisma/prisma.service.ts

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

@Injectable()
export class PrismaService extends PrismaClient {
  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}

src/prisma/prisma.module.ts

PrismaServiceをインポートしてNestJSアプリケーションにPrisma操作を認識させる。

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Swaggerのセットアップ

SwaggerはOpenAPIの仕様を使ってAPIをドキュメント化するためのツールである。NestJSには、Swaggerのための専用モジュールがある。

まずは必要な依存関係・ライブラリをインストールしよう。

npm install --save @nestjs/swagger swagger-ui-express

src/main.ts

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

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

  const config = new DocumentBuilder()
    .setTitle('Sample REST API')
    .setDescription('REST API Tutorial in NestJS')
    .setVersion('0.1')
    .build()
  
  const document = SwaggerModule.createDocument(app, config)
  SwaggerModule.setup('api', app, document)
  
  await app.listen(3000);
}
bootstrap();

ここで開発者サーバを立ち上げて、http://localhost:3000/api/にアクセスする。そうすれば、Swaggerの画面が表示されるだろう。

なお、Swaggerを導入する理由はREST APIの挙動を目視で確認できるようにするため。

CRUD操作の実装

ここから実際にREST APIを設計していく。NestJSでは、以下のコマンドで簡単にCRUD設計のプロトタイプを生成できる。

npx nest g resource

以下のように質問に回答する。

  1. What name would you like to use for this resource (plural, e.g., "users")?: users
  2. What transport layer do you use?: REST API
  3. Would you like to generate CRUD entry points?: Yes

これで、新しいsrc/usersディレクトリにRESTエンドポイント用の定型文がすべて揃った。

src/users/users.controller.tsファイル内には、異なるルート(ルートハンドラ)の定義がある。各リクエストを処理するビジネスロジックは、src/users/users.service.tsファイルにカプセル化される。

PrismaClientArticlesModuleに付け加える

PrismaClientArticlesModuleに付け加えるために、PrismaModuleをインポートしなければならない。以下のようにimportsのリストにUsersModuleを追加しておこう。

src/users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  imports: [PrismaModule]
})
export class UsersModule {}

これで、UsersServiceの中にPrismaServiceを注入し、それを使ってデータベースにアクセスできるようになりました。これを行うには、users.service.tsに以下のようなコンストラクタを追加します。

src/articles/articles.service.ts

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service'; // 追加

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {} // 追加

  create(createUserDto: CreateUserDto) {
    return 'This action adds a new user';
  }

  findAll() {
    return `This action returns all users`;
  }

  findOne(id: number) {
    return `This action returns a #${id} user`;
  }
  update(id: number, updateUserDto: UpdateUserDto) {
    return `This action updates a #${id} user`;
  }
  remove(id: number) {
    return `This action removes a #${id} user`;
  }
}

GETエンドポイントの実装

NestJSにおけるGETエンドポイントは以下のように表現される。(コントローラの場合)

// src/users/users.controller.ts

@Get()
findAll() {
  return this.usersService.findAll();
}

データベース内のすべての名前リストの配列を返すように、UsersService.findAll()を更新しなければならない。

findAll() {
  // return `This action returns all users`;
  return this.prisma.user.findMany()
}

このようにプログラムを書き換えて、http://localhost:3000/api/にアクセスしてドロップダウンメニューのGETをクリックして実行する。

GET /users/:idエンドポイントの実装

コントローラでは、findOneでデータベース内にあるデータの詳細を獲得できる。

src/users/users.controller.ts

@Get(':id')
findOne(@Param('id') id: string) {
  return this.usersService.findOne(+id);
}

こちらのルーティングは動的なidパラメータを受け取って、findOneコントローラルートハンドラに渡される。Articleモデルには整数のidフィールドがあるので、idパラメータは+演算子を使って数値にキャストしなければならない。

ここで、UsersServicefindOneメソッドを更新して、指定されたidを持つデータを返すようにする。

src/users/users.service.ts

findOne(id: number) {
  // return `This action returns a #${id} user`;
  return this.prisma.user.findUnique({ where: { id } });
}

http://localhost:3000/api/にアクセスしてドロップダウンメニューのGET /users/{id}をクリックする。

POST /usersエンドポイントの実装

NestJSでは、データベースにデータを新規で追加する場合はcreate関数を使う。

src/users/users.controller.ts

@Post()
create(@Body() createArticleDto: CreateArticleDto) {
  return this.articlesService.create(createArticleDto);
}

リクエストボディのなかでCreateArticleDtoタイプの引数を期待することに注目すること。DTO(Data Transfer Object)はデータがネットワーク上でどのように送信されるかを定義するオブジェクトだ。現在、CreateArticleDtoは空のクラスである。これにプロパティを追加してリクエストボディの形状を定義することになる。

src/users/dto/create-user.dto.ts

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


export class CreateUserDto {
  @ApiProperty()
  name: string

  @ApiProperty({ required: false })
  description?: string
}

クラスのプロパティをSwaggerModuleから見えるようにするには、@ApiProperty()デコレータが必要だ。

CreateArticleDtoは、Swagger APIページのSchemasに定義されている。UpdateArticleDtoCreateArticleDtoの定義から自動的に推論される。つまり、UpdateArticleDtoもSwagger内部で定義されていることになる。

src/users/users.service.ts

create(createUserDto: CreateUserDto) {
  // return 'This action adds a new user';
  return this.prisma.user.create({ data: createUserDto });
}

PATCH /users/:idの実装

こちらのエンドポイントは、既存の記事を更新するためのものである。このエンドポイントのためのルートハンドラはupdateと呼ばれる。

src/users/users.controller.ts

@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
  return this.usersService.update(+id, updateUserDto);
}

updateUserDtoの定義はCreateUserDtoPartialTypeとして定義されている。従って、これはCreateUserDtoのすべてのプロパティを持つことができる。

src/users/dto/update-user.dto.ts

import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';

export class UpdateArticleDto extends PartialType(CreateUserDto) {}

前述と同様に、この操作に対応するサービスメソッドを更新しなければならない。

src/users/users.service.ts

update(id: number, updateUserDto: UpdateUserDto) {
  // return `This action updates a #${id} user`;
  return this.prisma.user.update({
    where: { id },
    data: updateUserDto,
  })
}

user.update操作は、与えられたidを持つUserレコードを見つけ、updateUserDtoのデータでそれを更新しようとするもの。

データベース内にそのようなUserレコードが見つからない場合、Prismaはエラーを返す。このような場合、APIはユーザーフレンドリーなエラーメッセージを返さない。

DELETE /users/:idエンドポイントの実装

このエンドポイントは、既存のデータを削除するためのものである。このエンドポイントのためのルートハンドラはremoveと呼ばれる。以下のような感じだ。

src/users/users.controller.ts

@Delete(':id')
remove(@Param('id') id: string) {
  return this.usersService.remove(+id);
}

先程と同様に、UsersServiceに移動して対応するメソッドを更新する。

remove(id: number) {
  // return `This action removes a #${id} user`;
  return this.prisma.user.delete({ where: { id } })
}

これでDELETEエンドポイントの実装は終了する。

レスポンス型の設定

Swaggerの各エンドポイントの下にあるResponsesタブを見ると、Descriptionが空になっていることがわかる。これは、Swaggerがどのエンドポイントのレスポンスタイプも知らないからだ。いくつかのデコレーターを使ってこれを修正することになる。

まず、返されたエンティティオブジェクトの形状を識別するために、Swaggerが使用できるエンティティを定義する必要がある。これを行うには、articles.entity.tsファイル内のArticleEntityクラスを次のように更新しよう。

src/users/entities/user.entity.ts

import { ApiProperty } from "@nestjs/swagger";
import { User } from "@prisma/client";

export class UserEntity implements User {
    @ApiProperty()
    id: number

    @ApiProperty()
    name: string

    @ApiProperty({ required: false, nullable: true })
    description: string | null
}

Prisma Clientが生成するUser型を実装し、各プロパティに@ApiProperty()デコレータを追加したもの。

コントローラのルートハンドラに正しいレスポンスタイプをアノテートする。NestJSには、これを実装するのにデコレータがある。

src/users/users.controller.ts

import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';

@Controller('users')
@ApiTags('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @ApiCreatedResponse({ type: UserEntity })
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  @ApiOkResponse({ type: UserEntity, isArray: true })
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  @ApiOkResponse({ type: UserEntity })
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }

  @Patch(':id')
  @ApiOkResponse({ type: UserEntity })
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(+id, updateUserDto);
  }

  @Delete(':id')
  @ApiOkResponse({ type: UserEntity })
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}

GETPATCHDELETEエンドポイントには@ApiOkResponseを、POSTエンドポイントには@ApiCreatedResponseを追加している。typeプロパティは、戻り値の型を指定するために使用されます。

参照

Building a REST API with NestJS and Prisma - prisma.io