NestJS,MySQL,Prismaで簡易的なバックエンドアプリケーションを作成する

## はじめに
筆者が直近でNestJSとPrismaを扱う機会があったので、勉強目的で簡易的なSlackライクなアプリケーションを作成しました。

今回はその実行記録を記事でまとめたいと思います。

## 作成するもの

今回作成するアプリケーションは、Slackの基本的な機能を備えたバックエンドシステムです。具体的には以下の機能を実装します。

- チャンネル管理: ユーザーが複数のチャンネルを作成・一覧表示できる。
- メッセージ投稿: 各チャンネルに対してメッセージを投稿・取得・更新・削除できる。
- データベース操作: Prismaを使用してMySQLデータベースと連携し、データの永続化を行う。
- APIエンドポイント: NestJSを用いてRESTfulなAPIを構築し、フロントエンドからのリクエストを処理する。


## 1. 環境構築
### 1-1. NestJS, Prismaの準備
NestJS CLIのインストール、プロジェクトの作成をします。

```bash 
% npm install -g @nestjs/cli
% npx nest new slack-clone-backend
```

次にPrismaとその関連パッケージのインストールを行います。

```bash 
% cd slack-clone-backend
% npm install prisma --save-dev
% npm install @prisma/client
```


### 1-2. MySQLの準備
MySQLはdockerを使って準備します。(ローカルでMySQLを準備しても問題ありません。)


```bash 
% docker run -it --name slack-demo -e MYSQL_ROOT_PASSWORD=mysql -p 3306:3306 -d mysql:latest
```

:::note warn
既存のコンテナにはポートマッピングを後から追加できないため、忘れずにポートマッピングの設定を加えましょう。
:::


## 2.データベースのマイグレーション
まずmysqlで今回用いるデータベース`slack_clone`を作成します。

```bash 
% docker exec -it slack-demo mysql -u root -p
mysql> CREATE DATABASE slack_clone;
Query OK, 1 row affected (0.00 sec)

mysql> EXIT;
Bye
```

次にPrismaの初期化を行います。

```
% npx prisma init
```

これにより、`prisma`ディレクトリと`schema.prisma`ファイルが作成されます。

そして`prisma/schema.prisma`を以下のように編集します。

```prisma
datasource db {
  provider = "mysql"
  url      = "mysql://root:mysql@localhost:3306/slack_clone"
}

generator client {
  provider = "prisma-client-js"
}

model Channel {
  id      Int     @id @default(autoincrement())
  name    String
  posts   Post[]
}

model Post {
  id         Int      @id @default(autoincrement())
  content    String
  channel    Channel  @relation(fields: [channelId], references: [id])
  channelId  Int
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}
```


まず`datasource`にproviderとデータベースのurlを記載します。
ユーザーは`root`,パスワードは先ほど設定した`mysql`、データベース名は先ほど設定した`slack_clone`とします。

:::note warn
今回はデモなのでrootユーザーを使っています。
:::

今回チャンネルテーブル(`Channel`)と投稿テーブル(`Post`)の二つを作成したいので、`model`で定義します。

そしてデータベーススキーマを反映するために、Prismaのマイグレーションを実行します。

```bash 
% npx prisma migrate dev --name init
```

## 3.初期データの挿入

`prisma/seed.ts`ファイルを作成し、初期データを挿入します。
今回はチャンネルを二つ作っておきます。

```typescript
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  const channel1 = await prisma.channel.create({
    data: { name: 'general' },
  });

  const channel2 = await prisma.channel.create({
    data: { name: 'random' },
  });
}

main()
  .catch((e) => console.error(e))
  .finally(async () => {
    await prisma.$disconnect();
  });
```

`package.json`にシードスクリプトを追加します。

```bash
{
  "scripts": {
    // 他のスクリプト
    "seed": "ts-node prisma/seed.ts"
  }
}

```

初期データをDBに挿入します。

```bash 
% npm run seed
```

初期データが挿入されているかは実際にmysqlの中身を以下のように確認します。

```bash 
% docker exec -it slack-demo mysql -u root -p
mysql> USE slack_clone;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SHOW TABLES;
+-----------------------+
| Tables_in_slack_clone |
+-----------------------+
| Channel               |
| Post                  |
| _prisma_migrations    |
+-----------------------+
3 rows in set (0.01 sec)

mysql> SELECT * FROM Channel;
+----+---------+
| id | name    |
+----+---------+
|  1 | general |
|  2 | random  |
+----+---------+
2 rows in set (0.00 sec)

mysql>
```

実際に`Channel`、`Table`テーブルが作成され、`Channel`に二つのデータが挿入されていることが確認できました。



## 4.モジュール、サービス、コントローラの作成
チャンネルモジュールと投稿モジュールを作成します。

```bash 
% npx nest g module channel
% npx nest g controller channel --no-spec
% npx nest g service channel --no-spec
% npx nest g module posts
% npx nest g controller posts --no-spec
% npx nest g service posts --no-spec
```

PrismaクライアントをNestJSで使用するためのサービスを作成します。

```bash 
% npx nest g module prisma
% npx nest g service prisma --no-spec
```

実装は長いので畳んでおきます。

<details>
<summary>実装はこちら</summary>

### prisma関連
```typescript
// prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService]
})
export class PrismaModule {}


// prisma/prisma.service.ts
import { Injectable, OnModuleInit, INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }

   async enableShutdownHooks(app: INestApplication) {
     process.on('beforeExit', () => {
       app.close();
     });
   }
}
```

### チャンネル関連
```typescript
// channels/channles.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { ChannelsController } from './channels.controller';
import { ChannelsService } from './channels.service';

@Module({
  imports: [PrismaModule],
  controllers: [ChannelsController],
  providers: [ChannelsService]
})
export class ChannelsModule {}


// channels/channles.controller.ts
import { Controller, Get } from '@nestjs/common';
import { ChannelsService } from './channels.service';

@Controller('channels')
export class ChannelsController {
  constructor(
    private readonly channelsService: ChannelsService,
  ) {}

  @Get()
  findAll() {
    return this.channelsService.findAll();
  }
}

// channels/channles.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class ChannelsService {
  constructor(private prisma: PrismaService) {}

  async findAll() {
    return this.prisma.channel.findMany();
  }
}
```

### 投稿関連
```typescript
//  posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';

@Module({
  imports: [PrismaModule],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}


// posts/posts.controller.ts
import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller()
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get('channels/:id/posts')
  findByChannel(@Param('id') id: string) {
    return this.postsService.findByChannel(+id);
  }

  @Get('posts')
  findAll() {
    return this.postsService.findAll();
  }

  @Post('channels/:id/posts')
  create(@Param('id') id: string, @Body('content') content: string) {
    return this.postsService.create(+id, content);
  }

  @Put('posts/:id')
  update(@Param('id') id: string, @Body('content') content: string) {
    return this.postsService.update(+id, content);
  }

  @Delete('posts/:id')
  remove(@Param('id') id: string) {
    return this.postsService.remove(+id);
  }
}

// posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {}

  async findByChannel(channelId: number) {
    return this.prisma.post.findMany({
      where: { channelId },
    });
  }
  
  async findAll() {
    return this.prisma.post.findMany();
  }

  async create(channelId: number, content: string) {
    return this.prisma.post.create({
      data: {
        content,
        channel: {
          connect: { id: channelId },
        },
      },
    });
  }

  async update(id: number, content: string) {
    return this.prisma.post.update({
      where: { id },
      data: { content },
    });
  }

  async remove(id: number) {
    return this.prisma.post.delete({
      where: { id },
    });
  }
}
```

### 全体

```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ChannelsModule } from './channels/channels.module';
import { PostsModule } from './posts/posts.module';
import { PrismaModule } from './prisma/prisma.module';

@Module({
  imports: [ChannelsModule, PostsModule, PrismaModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

</details>

## 5.実行確認
まずアプリケーションを起動します。

```
% npm run start
```
アプリケーションが`http://localhost:3000`で起動していることを確認します。


### 5-1. チャンネルの確認
以下のコマンドでチャンネル一覧を取得します。

```bash 
% curl http://localhost:3000/channels/       
[{"id":1,"name":"general"},{"id":2,"name":"random"}]
```

### 5-2. 投稿データ追加と確認

1つ目のチャンネルに2つ、2つ目のチャンネルに1つの投稿します。

```bash 
% curl -X POST -H "Content-Type: application/json" -d '{"content":"こんにちは、世界！"}' http://localhost:3000/channels/1/posts
% curl -X POST -H "Content-Type: application/json" -d '{"content":"おはよう、世界！"}' http://localhost:3000/channels/1/posts
% curl -X POST -H "Content-Type: application/json" -d '{"content":"こんばんは、世界！"}' http://localhost:3000/channels/2/posts
```

投稿全体を確認します。

```bash 
% curl http://localhost:3000/posts           
[{"id":1,"content":"こんにちは、世界！","channelId":1,"createdAt":"2024-09-20T06:18:10.882Z","updatedAt":"2024-09-20T06:18:10.882Z"},{"id":2,"content":"おはよう、世界！","channelId":1,"createdAt":"2024-09-20T06:19:34.851Z","updatedAt":"2024-09-20T06:19:34.851Z"},{"id":3,"content":"こんばんは、世界！","channelId":2,"createdAt":"2024-09-20T06:34:59.223Z","updatedAt":"2024-09-20T06:34:59.223Z"}]
```

各チャンネルごとの投稿を確認します。

```bash 
% curl http://localhost:3000/channels/1/posts
[{"id":1,"content":"こんにちは、世界！","channelId":1,"createdAt":"2024-09-20T06:18:10.882Z","updatedAt":"2024-09-20T06:18:10.882Z"},{"id":2,"content":"おはよう、世界！","channelId":1,"createdAt":"2024-09-20T06:19:34.851Z","updatedAt":"2024-09-20T06:19:34.851Z"}
% curl http://localhost:3000/channels/2/posts
[{"id":3,"content":"こんばんは、世界！","channelId":2,"createdAt":"2024-09-20T06:34:59.223Z","updatedAt":"2024-09-20T06:34:59.223Z"}]
```

### 5-3. 投稿データ更新と確認

一つ目の投稿を更新します。

```bash 
% curl -X PUT -H "Content-Type: application/json" -d '{"content":"更新した投稿"}'  http://localhost:3000/posts/1
```

実際に更新されているかを確認します。

```bash 
% curl http://localhost:3000/posts
[{"id":1,"content":"更新した投稿","channelId":1,"createdAt":"2024-09-20T06:18:10.882Z","updatedAt":"2024-09-20T06:38:18.894Z"},{"id":2,"content":"おはよう、世界！","channelId":1,"createdAt":"2024-09-20T06:19:34.851Z","updatedAt":"2024-09-20T06:19:34.851Z"},{"id":3,"content":"こんばんは、世界！","channelId":2,"createdAt":"2024-09-20T06:34:59.223Z","updatedAt":"2024-09-20T06:34:59.223Z"}]% 
```

### 5-4. 投稿データ削除と確認

一つ目の投稿を削除します。

```bash 
% curl -X DELETE http://localhost:3000/posts/1
```

投稿が削除されているかどうかを確認します。

```bash 
% curl http://localhost:3000/posts            
[{"id":2,"content":"おはよう、世界！","channelId":1,"createdAt":"2024-09-20T06:19:34.851Z","updatedAt":"2024-09-20T06:19:34.851Z"},{"id":3,"content":"こんばんは、世界！","channelId":2,"createdAt":"2024-09-20T06:34:59.223Z","updatedAt":"2024-09-20T06:34:59.223Z"}
```

## おわりに
今回の記事では、NestJSとPrismaを使用して簡易的なSlackライクなアプリケーションのバックエンドを構築しました。主なステップとして、環境構築、データベースのマイグレーション、初期データの挿入、モジュール・サービス・コントローラの作成、そしてAPIの実行確認を行いました。


👇 続きはこちら

https://qiita.com/sergicalsix/items/f6cb314082bc1fc214e5