こちらのリポジトリは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
- Prismaのセットアップ
- データベースのマイグレーション
- データベースのデータ作成、
prisma
サービスの作成 - Swaggerのセットアップ
GET
、POST
、PUT
、DELETE
メソッドの実装- レスポンス型の設定
まずは以下のコマンドを入力する。
# 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
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!')
});
});
});
単一の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()
}
}
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 {}
単一のメソッドを持つ基本的なService
。
import { Injectable } from '@nestjs/common'
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!'
}
}
コア関数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は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
以下のコマンドで、NestJSアプリ開発に必要なModule
とService
をインストールしておく
npx nest g module prisma
npx nest g service prisma
これによって、新しいサブディレクトリ./src/prisma
が生成され、prisma.module.ts
とprisma.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は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の挙動を目視で確認できるようにするため。
ここから実際にREST APIを設計していく。NestJSでは、以下のコマンドで簡単にCRUD設計のプロトタイプを生成できる。
npx nest g resource
以下のように質問に回答する。
What name would you like to use for this resource (plural, e.g., "users")?
: usersWhat transport layer do you use?
: REST APIWould you like to generate CRUD entry points?
: Yes
これで、新しいsrc/users
ディレクトリにRESTエンドポイント用の定型文がすべて揃った。
src/users/users.controller.ts
ファイル内には、異なるルート(ルートハンドラ)の定義がある。各リクエストを処理するビジネスロジックは、src/users/users.service.ts
ファイルにカプセル化される。
PrismaClient
をArticles
Moduleに付け加えるために、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`;
}
}
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
をクリックして実行する。
コントローラでは、findOne
でデータベース内にあるデータの詳細を獲得できる。
src/users/users.controller.ts
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
こちらのルーティングは動的なid
パラメータを受け取って、findOne
コントローラルートハンドラに渡される。Article
モデルには整数のid
フィールドがあるので、id
パラメータは+
演算子を使って数値にキャストしなければならない。
ここで、UsersService
のfindOne
メソッドを更新して、指定された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}
をクリックする。
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に定義されている。UpdateArticleDto
はCreateArticleDto
の定義から自動的に推論される。つまり、UpdateArticleDto
もSwagger内部で定義されていることになる。
src/users/users.service.ts
create(createUserDto: CreateUserDto) {
// return 'This action adds a new user';
return this.prisma.user.create({ data: createUserDto });
}
こちらのエンドポイントは、既存の記事を更新するためのものである。このエンドポイントのためのルートハンドラはupdate
と呼ばれる。
src/users/users.controller.ts
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}
updateUserDto
の定義はCreateUserDto
のPartialType
として定義されている。従って、これは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はユーザーフレンドリーなエラーメッセージを返さない。
このエンドポイントは、既存のデータを削除するためのものである。このエンドポイントのためのルートハンドラは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);
}
}
GET
、PATCH
、DELETE
エンドポイントには@ApiOkResponse
を、POSTエンドポイントには@ApiCreatedResponse
を追加している。type
プロパティは、戻り値の型を指定するために使用されます。