1. 要件の整理
まずは要件の整理から。今回実現したい機能は下記の5つです。
- 作成機能:新しいToDoを1つ作成する機能
- 一覧表示機能:全てのToDoを一覧表示する機能
- 詳細表示機能:1つのToDoの詳細情報を表示する機能
- 更新機能:既存のToDoを編集して更新する機能
- 削除機能:既存のToDoを削除する機能
2. API設計
次に、先ほど挙げた5機能をAPIとしてどう実装するかを設計していきましょう。
設計といっても要件がシンプルなので簡単なもので大丈夫です。
- 作成機能:ToDoのタスク名(name)と期限(deadline)を受け取り、idをキーにデータベースに登録する。
- 一覧表示機能:リクエストを受け取り、データベースからToDoの全量を取得してレスポンスとして返す。
- 詳細表示機能:パラメータにidを含むリクエストを受け取り、idに該当するToDo1件をデータベースから取得してレスポンスとして返す。
- 更新機能:ToDoのタスク名(name)または期限(deadline)または両方を受け取り、idをキーにデータベースの該当データを更新する。
- 削除機能:パラメータにidを含むリクエストを受け取り、idに該当するToDo1件をデータベースから削除する。
これでざっくりAPI設計完了です。
3. NestJSの構造の理解
Part1の4–2. リソースの作成でnest g resource todos
というコマンドを実行したので、ある程度テンプレートが作られています。
実装に入る前に、テンプレートの中身を見て、NestJSの構造を理解するところから進めましょう。
主なファイルは以下の通りです。
todo-app
└──todo-api
└──src
├──main.ts # アプリケーションのエントリポイント
├──app.module.ts # アプリケーションのルートモジュール
└──todos
├──todos.module.ts # モジュール:コンポーネントを1つにまとめる
├──todos.controller.ts # コントローラ:ルーティングを定義
└──todos.service.ts # サービス:ビジネスロジックを定義
3–1. main.ts(エントリポイント)
// main.ts
...
async function bootstrap() {
const app = await NestFactory.create(AppModule); # NestJSのインスタンスを作成
await app.listen(3000); # ポート3000でアプリケーションを起動
}
bootstrap();
このファイルはアプリケーションのエントリポイント(実行の開始点)です。
NestFactory.create(appModule)
でNestJSのインスタンスを作成しています。
app.listen(3000)
の部分でポート番号を指定しているので、ローカルホストで実行すると、http://localhost:3000
で起動します。
3–2. app.module.ts(ルートモジュール)
// app.module.ts
...
@Module({
imports: [TypeOrmModule.forRoot(AppDataSource.options), TodosModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
このファイルは先ほどのmain.ts
でインスタンス化していたAppModule
を定義しています。
AppModule
はアプリケーション全体のルートモジュールです。
NestJSのモジュールとは、@Module()
デコレータを付与したクラスのことで、役割ごとにコンポーネントを1つにまとめる役割を担います。
imports:
の中にTodosModule
がありますが、このように記述することで、各モジュールをルートモジュールから利用することができます。
3–3. todos.module.ts(モジュール)
// todos.module.ts
...
@Module({
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
このファイルは先ほどのapp.module.ts
でimportしていたTodosModule
を定義しています。
TodosModule
はToDoに関する機能をまとめたモジュールです。
controllers:
の中にTodosController
を記述することでコントローラを定義し、providers:
の中にTodosService
を記述することでサービスを定義します。
コントローラとサービスについてはこの後説明します。
3–4. todos.controller.ts(コントローラ)
// todos.controller.ts
...
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}
@Get()
findAll() {
return this.todosService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.todosService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateTodoDto: UpdateTodoDto) {
return this.todosService.update(+id, updateTodoDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.todosService.remove(+id);
}
}
コントローラは@Controller()
デコレータを付与したクラスで、指定したパスでリクエストを受け取り、レスポンスを返す、ルーティングの役割を担います。
ここでは、/todos
というパスに5つのメソッドが定義されています。
メソッドに付与されている@Get()
などはHTTPメソッドの種類を表しており、引数の@Body()
はリクエストボディ、@Param()
はパラメータを表しています。
メソッド内の実際の処理は次で説明するサービスで定義されています。
3–5. todos.service.ts(サービス)
// todos.service.ts
...
@Injectable()
export class TodosService {
create(createTodoDto: CreateTodoDto) {
return 'This action adds a new todo';
}
findAll() {
return `This action returns all todos`;
}
findOne(id: number) {
return `This action returns a #${id} todo`;
}
update(id: number, updateTodoDto: UpdateTodoDto) {
return `This action updates a #${id} todo`;
}
remove(id: number) {
return `This action removes a #${id} todo`;
}
}
サービスは@Injectable()
デコレータを付与したクラスで、ロジックを記載する場所です。
先ほどのコントローラの実装(下記)を見るとわかるように、サービスクラスはコントローラクラスのコンストラクタに依存性注入(Dependency Injection)する形で渡されています。
// todos.controller.ts
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
...
}
4. 実装・検証
テンプレートの内容をざっと確認したところで、いよいよ実装に入っていきます。
実装が正しく動くかの検証も並行して行っていきましょう。
検証を行うために、下記コマンドでデータベース(Docker)とAPIを予め起動しておきます。
# Dockerでデータベースを起動
docker-compose up -d
# APIを起動
cd todo-api
pnpm run start:dev
次に、todos.module.ts
にTodoエンティティをインポートします。これでTodosModule
でTodoエンティティの操作ができるようになります。
// todos.module.ts
import { Module } from '@nestjs/common';
import { TodosService } from './todos.service';
import { TodosController } from './todos.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './entities/todo.entity';
@Module({
imports: [TypeOrmModule.forFeature([Todo])], // Todoエンティティをインポート
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
4–1. 作成機能
ToDoのタスク名(name)と期限(deadline)を受け取り、idをキーにデータベースに登録する機能を実装します。
これにはtodos.controller.ts
で定義されているcreate()
メソッドが使えそうです。
// todos.controller.ts
...
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}
...
コントローラのcreate()
メソッドを見てみると、引数には@Body()
デコレータが付与されているので、CreateTodoDto
型のリクエストボディが渡されており、戻り値としてサービスクラスのcreate()
メソッドの実行結果を返していることが分かります。
まず、引数の型であるCreateTodoDto
を見てみましょう。下記のcreate-todo.dto.ts
で定義されています。
todo-app
└──todo-api
└──src
└──todos
└──dto
└──create-todo.dto.ts
DTOはデータを受け渡す際に1つにまとめるためのオブジェクトで、NestJSではデータの型定義やバリデーションを実装できます。
デフォルトではクラス定義しかされていないので、ToDoのタスク名(name)と期限(deadline)を定義しておきましょう。
// create-todo.dto.ts
export class CreateTodoDto {
name: string; // タスク名を定義
deadline: Date; // タスク期限を定義
}
次にサービスクラスの実装です。
サービスクラス内でTodosエンティティを操作するために、Todosリポジトリを定義します。
// todos.service.ts
...
import { InjectRepository } from '@nestjs/typeorm';
import { Todo } from './entities/todo.entity';
import { Repository } from 'typeorm';
@Injectable()
export class TodosService {
constructor(
@InjectRepository(Todo)
private todosRepository: Repository<Todo>, // Todosリポジトリを定義
) {}
...
}
データの作成処理はリポジトリのsave()
メソッドで実装できるので、サービスクラスのcreate()
メソッド内に実装します。また、データ操作を行うため、非同期処理に修正しましょう。
// todos.service.ts
...
async create(createTodoDto: CreateTodoDto): Promise<Todo> {
return await this.todosRepository.save(createTodoDto);
}
...
同様にコントローラクラスのcreate()
メソッドも非同期処理に修正します。これで作成処理の実装完了です。
// todos.controller.ts
...
@Post()
async create(@Body() createTodoDto: CreateTodoDto): Promise<CreateTodoDto> {
return await this.todosService.create(createTodoDto);
}
...
実装が正しく動くかを簡単に確かめるために、curlコマンドを使いましょう。
ターミナルを開いて下記コマンドを実行し、http://localhost:3000/todos
に対してJSON形式のボディとともにPOSTリクエストを送信します。
# /todosに対してPOSTリクエストを送信
curl -X POST 'http://localhost:3000/todos' -d '{ "name": "1つ目のタスク", "deadline": "2024-05-31" }' -H 'Content-Type: application/json'
送信したリクエストボディが返ってきたら成功です。
# 実行結果
{"name":"タスク1","deadline":"2024-05-31","id":1}
4.2 一覧表示機能
リクエストを受け取り、データベースからToDoの全量を取得してレスポンスとして返す機能を実装します。
コントローラクラスのfindAll()
メソッドを実装します。
// todos.controller.ts
...
@Get()
async findAll(): Promise<Todo[]> {
return await this.todosService.findAll();
}
...
サービスクラスのfindAll()
メソッドを実装します。
// todos.service.ts
...
async findAll(): Promise<Todo[]> {
return await this.todosRepository.find();
}
...
動作確認の前に、一覧表示されていることが分かりやすいようにもう1つタスクを追加しておきます。
# /todosに対してPOSTリクエストを送信
curl -X POST 'http://localhost:3000/todos' -d '{ "name": "2つ目のタスク", "deadline": "2024-05-31" }' -H 'Content-Type: application/json'
curlコマンドで下記のGETリクエストを送信しましょう。
# /todosに対してGETリクエストを送信
curl -X GET 'http://localhost:3000/todos'
作成した2つのデータが返ってきたら成功です。
# GETリクエストの実行結果
[{"id":1,"name":"1つ目のタスク","deadline":"2024-05-30T15:00:00.000Z"}, {"id":2,"name":"2つ目のタスク","deadline":"2024-05-30T15:00:00.000Z"}]
4.3 詳細表示機能
パラメータにidを含むリクエストを受け取り、idに該当するToDo1件をデータベースから取得してレスポンスとして返す機能を実装します。
コントローラクラスのfindOne()
メソッドを実装します。
// todos.controller.ts
...
@Get(':id')
async findOne(@Param('id') id: string): Promise<Todo> {
return await this.todosService.findOne(id);
}
...
サービスクラスのfindOne()
メソッドを実装します。{ where: { id: id } }
の部分でidの値を渡すことで、idをキーにデータを取得する処理が実現できます。
// todos.service.ts
...
async findOne(id: string): Promise<Todo> {
return await this.todosRepository.findOne({
where: { id: id },
});
}
...
curlコマンドでidを指定してGETリクエストを送信しましょう。
# /todos/1に対してGETリクエストを送信
curl -X GET'http://localhost:3000/todos/1'
idが1のデータが返ってきたら成功です。
# GETリクエストの実行結果
{"id":1,"name":"1つ目のタスク","deadline":"2024-05-30T15:00:00.000Z"}
4.4 更新機能
ToDoのタスク名(name)と期限(deadline)を受け取り、idをキーにデータベースの該当データを更新する機能を実装します。
コントローラクラスのupdate()
メソッドを実装します。
// todos.controller.ts
...
@Patch(':id')
async update(
@Param('id') id: string,
@Body() updateTodoDto: UpdateTodoDto,
): Promise<UpdateResult> {
return await this.todosService.update(id, updateTodoDto);
}
...
引数の型であるUpdateTodoDto
を定義しているupdate-todo.dto.ts
はデフォルトのままにしておきます。
// update-todo.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends PartialType(CreateTodoDto) {}
サービスクラスのupdate()
メソッドを実装します。
// todos.service.ts
...
async update(
id: string,
updateTodoDto: UpdateTodoDto,
): Promise<UpdateResult> {
return await this.todosRepository.update(id, updateTodoDto);
}
...
curlコマンドでidを指定してPATCHリクエストを送信し、idが1のデータのタスク名を更新してみましょう。
# /todos/1に対してPATCHリクエストを送信
curl -X PATCH 'http://localhost:3000/todos/1' -d '{ "name": "卵を買う" }' -H 'Content-Type: application/json'
# PATCHリクエストの実行結果
{"generatedMaps":[],"raw":[],"affected":1}
GETリクエストで確認し、タスク名が更新されていれば成功です。
# /todos/1に対してGETリクエストを送信
curl -X GET 'http://localhost:3000/todos/1'
# GETリクエストの実行結果
{"id":1,"name":"卵を買う","deadline":"2024-05-30T15:00:00.000Z"}
4–5. 削除機能
最後に、パラメータにidを含むリクエストを受け取り、idに該当するToDo1件をデータベースから削除する機能を実装します。
コントローラクラスのremove()
メソッドを実装します。
// todos.controller.ts
...
@Delete(':id')
async remove(@Param('id') id: string) {
return await this.todosService.remove(id);
}
...
サービスクラスのremove()
メソッドを実装します。
// todos.service.ts
...
async remove(id: string) {
return await this.todosRepository.delete(id);
}
...
curlコマンドでidを指定してDELETEリクエストを送信し、タスクを削除してみましょう。
# /todos/1に対してDELETEリクエストを送信
curl -X DELETE 'http://localhost:3000/todos/1'
# DELETEリクエストの実行結果
{"raw":[],"affected":1}
タスクが削除されたか確認します。idが1のタスクが削除されていれば成功です。
# /todosに対してGETリクエストを送信
curl -X GET 'http://localhost:3000/todos'
# GETリクエストの実行結果
[{"id":2,"name":"タスク2","deadline":"2024-05-30T15:00:00.000Z"}]
以上でAPIの実装は完了です。
次回はNestJSを使って画面を実装していきます!
We are hiring!
本記事をご覧いただき、ネクストビートの技術や組織についてもっと話を聞いてみたいと思われた方、カジュアルにお話しませんか?
・今後のキャリアについて悩んでいる
・記事だけでなく、より詳しい内容について知りたい
・実際に働いている人の声を聴いてみたい
など、まだ転職を決められていない方でも、ネクストビートに少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話しましょう!
🔽申し込みはこちら
https://hrmos.co/pages/nextbeat/jobs/1000008
また、ネクストビートについてはこちらもご覧ください。