Todoアプリを作成してNest.jsのキャッチアップをしてみた

## はじめに

後述の記事を参考にNest.jsのキャッチアップを行いました。
ワンポイント追加として、DockerでDB（Postgres）を構築し、そこに接続するように設定してみました。
私が普段使っているバックエンドフレームワークはLaravelですが、そもそもの設計思想の違いなのかすんなりと理解が進まなかったので備忘録として記載します。

この記事の内容はほとんど下記２記事の内容をなぞったものです。
下記の記事に従って動かないとか、どんなふうに読んでいるんだろう的な時の参考になれば幸いです。

## リポジトリ

[https://github.com/ryosuke-horie/nest-todo-app/](https://github.com/ryosuke-horie/nest-todo-app/)

## 参考記事
以下の2記事を参考に進めています。

### Next.js
[NestJSを触りながら学ぶ(TodoAPI作成)](https://zenn.dev/kobayashiyabako/articles/nestjs-first)

### Docker postgresql
[【Docker】postgresqlの構築](https://zenn.dev/re24_1986/articles/b76c3fd8f76aec)

## 開発環境

- Windows 10
- WSL2
- node v18.16.0
- nest v10.1.9

## ****インストール****

```bash
$ npm i -g @nestjs/cli
$ nest new NestTodoApp // npm を使用するように指定する
$ npm run start
```

![rapture_20230717194128.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/781232/927e6fc6-6ec8-a2d3-fbdd-f76fe462c749.jpeg)


初期画面がすごくシンプル。

## ****データベース作成****

`docker-compose.yml`をルートに作成

```yaml
version: '3'

services:
  db:
    image: postgres:14
    container_name: postgres
    ports:
      - 5432:5432
    volumes:
      - db-store:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=passw0rd
volumes:
  db-store:
```

environmentに必須項目のパスワードのみを指定

DB起動

```yaml
docker-compose up -d
```

A5M2(DB接続ツール)を利用して、接続。

- • DBファイルのパス(`/var/lib/postgresql/data`)をvolumeにマウントして永続化しています。

## ****ファイル削除****

記事に従う。

## 必要パッケージのインストール

```bash
npm i @nestjs/typeorm typeorm pg @nestjs/config class-validator class-transformer
```

## 開発

### 構成を確認

### CLIでモジュールを生成

`nest g module [module名]`で自動生成できる

```bash
nest g module tasks
```

### CLIでControllerを作成

```bash
nest g controller tasks --no-spec
```

`--no-spec`オプションによってテスト用のファイルを生成しないようにしている。

### Controllerを修正

記事の通り

```tsx
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common';

@Controller('tasks')
export class TasksController {
    
    @Get()
    getTasks() {
        return "getTasks Success!"
    }

    @Get('/:id')
    getTaskById(
        @Param('id', ParseIntPipe) id: number) {
        return `getTaskById Success! Parameter [id:${id}]`
    }

    @Post()
    createTask(
        @Body('title') title: string,
        @Body('description') description: string) {
        return `createTask Success! Prameter [title:${title}, descritpion:${description}]`
    }

    @Delete('/:id')
    deleteTask(
        @Param('id', ParseIntPipe) id: number) {
        return `deleteTask Success! Prameter [id:${id}]`
    }

    @Patch('/:id')
    updateTask(
        @Param('id', ParseIntPipe) id: number,
        @Body('status') status: string ) {
        return `updateTask Success! Prameter [id:${id}, status:${status}]`
    }
}
```

- `@Param()`や`@Body()`はHTTPリクエストを取得するためのデコレータ
- `ParseIntPipe`はパイプという機能の1つ
    - コントローラのメソッドに値が引き渡される前に**変換**、もしくは**検証**を行います。
    - `@Param('id', ParseIntPipe)`ではidを数値型へと変換
    

開発中は`npm run start:dev`を使えば変更が即時反映できる。

## ****DTO・PIPEを作成してPOSTメソッドにバリデーションを追加****

記事に従ってDTOとPIPEを作成

Controllerに追記して、POSTメソッドのバリデーションを追加する。

バリデーションの中身は

- statusが'OPEN','PROGRESS','DONE'のいづれかでないとエラーを返す
- TitleとDescriptionの値が空ならエラーを返す

## Service作成

```tsx
nest g service tasks
```

`tasks.module.ts`に自動で追記される。

## TypeORMの準備

### 接続設定 app.module.ts

ここはDockerを使っているので設定が一部変更している部分。

```tsx
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';

@Module({
  imports: [
    TasksModule,
    // importsに追加
    TypeOrmModule.forRoot({
      type: 'postgres',       // DBの種類
      port: 5432,             // 使用ポート
      database: 'postgres',    // データベース名
      host: 'localhost',      // DBホスト名
      username: 'postgres',       // DBユーザ名
      password: 'passw0rd',       // DBパスワード
      synchronize: true,      // モデル同期(trueで同期)
      entities: [__dirname + '/**/*.entity.{js,ts}'],  // ロードするエンティティ
    })
  ],
})
export class AppModule {}
```

## ****Entitiyの作成****

task.entity.tsを作成し、以下を記述

```tsx
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class Task extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    description: string;

    @Column()
    status: string;
}
```

この時点でA5M2で確認するとtaskテーブルが作成されている。

## 環境変数の更新

DBを同期するように設定しているが、本番でこの運用をするとよくないので記事に従って.envファイルを作成

## ****TypeORMのConfigService****

DBのパスワードに数字が盛り込まれている影響か、エラーが出たため、.env/default.envの値をシングルクオーテーションで囲んだ。

main.tsを書き替えてみるとDB_SYNCの値が設定されていなかったが、

```tsx
NODE_ENV=development npm run start:dev
```

を利用することで反映されていることが確認できました。

## Serviceの修正

記事に従って修正

Controllerから受け取った値で実処理するよう構成している。

```tsx
async getTaskById(id: number): Promise<Task> {
        const found = await this.taskRepository.findOne(id);
```

ここでエラー：型 'number' には型 'FindOneOptions<Task>' と共通のプロパティがありません。

が出たので以下のように仮で修正

```tsx
const found = await this.taskRepository.findOne({
            where: {
              id: id,
            },
          })
```

## テストを作成する前に****Module & Controller修正に進む****

エラーが出るため。

Repositoryのimportがmoduleで不足してエラーが出るため。

## テストを記述して実行

passしたので完了。

## 感想

ふわっとREST APIを実装できました。元記事を作成された方がすごいです。

TypeORMのドキュメントを読み込む必要があるかと思います。

ワンポイント追加でDockerでDBをローカル環境に作成して利用するように設定できました。

個人的にはEntityやRepositoryといったデザインパターン的な部分についても追加で学ぶ必要があるかと思います。