簡単なREST APIを開発する方法(Prisma)

開発環境

  • 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. 1.
    Prismaのセットアップ
  2. 2.
    データベースのマイグレーション
  3. 3.
    データベースのデータ作成、prismaサービスの作成
  4. 4.
    Swaggerのセットアップ
  5. 5.
    GETPOSTPUTDELETEメソッドの実装
  6. 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ファイルになる。このような感じで、NestJSとSQLiteを連携できます。
-- 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ファイルを作成します。
src/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();
});
}
}
PrismaServiceをインポートしてNestJSアプリケーションにPrisma操作を認識させましょう。これで基本的なセットアップは完了です。
src/prisma/prisma.module.ts
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. 1.
    What name would you like to use for this resource (plural, e.g., "users")?: users
  2. 2.
    What transport layer do you use?: REST API
  3. 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/user.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/user.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プロパティは、戻り値の型を指定するために使用されます。

参照

Copy link
On this page
開発環境
大まかな手順
初期設定
開発者サーバの立ち上げ
簡単なディレクトリ紹介
app.controller.spec.ts
app.controller.ts
app.module.ts
app.service.ts
main.ts
Prismaのセットアップ
データベースの設計
モデルを設計する
データベースのマイグレーション(接続)
データベースの作成
Prismaサービスの作成
Swaggerのセットアップ
CRUD操作の実装
PrismaClientをArticlesModuleに付け加える
GETエンドポイントの実装
GET /users/:idエンドポイントの実装
POST /usersエンドポイントの実装
PATCH /users/:idの実装
DELETE /users/:idエンドポイントの実装
レスポンス型の設定
参照