【SvelteKit×NestJS】ゼロからアプリを作ってみよう!Part2〜API実装〜

Yuki Hamakawa
nextbeat-engineering
21 min readMay 2, 2024

はじめに

こんにちは。ネクストビート保育士バンク!という保育士向け転職支援サービスを開発している浜川勇輝です!

本記事は、保育士バンク!で採用しているSvelteKitNestJSを使って、簡単なToDoアプリを作成するシリーズのPart2です。

ToDoの作成、表示、編集、削除というCRUD処理が一通りできるアプリをゼロから作っていきます。

今回のゴール

Part1ではデータベースの構築を行いました(詳細はこちらの記事をご覧ください)ので、今回はNestJSを使ったAPIの実装を行います。

それでは作っていきましょう!

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の構造の理解

Part14–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

また、ネクストビートについてはこちらもご覧ください。

🔽エントランスブック
https://note.nextbeat.co.jp/n/nd6f64ba9b8dc

--

--