はじめに
筆者が直近で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>
実際にChannel
、Table
テーブルが作成され、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の実行確認を行いました。
👇 続きはこちら