1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-09-24

はじめに

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

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

作成するもの

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

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

1. 環境構築

1-1. NestJS, Prismaの準備

NestJS CLIのインストール、プロジェクトの作成をします。

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

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

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

1-2. MySQLの準備

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

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

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

2.データベースのマイグレーション

まずmysqlで今回用いるデータベースslack_cloneを作成します。

% 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を以下のように編集します。

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とします。

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

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

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

% npx prisma migrate dev --name init

3.初期データの挿入

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

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にシードスクリプトを追加します。

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

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

% npm run seed

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

% 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>

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

4.モジュール、サービス、コントローラの作成

チャンネルモジュールと投稿モジュールを作成します。

% 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で使用するためのサービスを作成します。

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

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

実装はこちら

prisma関連

// 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();
     });
   }
}

チャンネル関連

// 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();
  }
}

投稿関連

//  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 },
    });
  }
}

全体

// 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 {}

5.実行確認

まずアプリケーションを起動します。

% npm run start

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

5-1. チャンネルの確認

以下のコマンドでチャンネル一覧を取得します。

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

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

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

% 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

投稿全体を確認します。

% 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"}]

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

% 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. 投稿データ更新と確認

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

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

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

% 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. 投稿データ削除と確認

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

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

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

% 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の実行確認を行いました。

👇 続きはこちら

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?