優れたNest.jsブログを構築する:コメントシステム
Takashi Yamamoto
Infrastructure Engineer · Leapcell

前のチュートリアルでは、express-session
を使用してユーザー認証を実装しました。これにより、ユーザーはブログに登録およびログインできるようになり、ログインが必要なルートも保護されました。
読者と作成者を明確に区別できるようになったので、両者間のインタラクションのための機能を追加するのに最適な時期ではないでしょうか?
この記事では、ブログに基本的でありながら非常にコアな機能、つまりコメントシステムを追加します。
具体的には、以下の機能を実装します。
- 各記事の下にコメントリストを表示する。
- ログインしたユーザーがコメントを投稿できるようにする。
ステップ1:コメントのデータモデルを作成する
以前の投稿やユーザーと同様に、コメントにも独自のデータベーステーブルと対応するEntityファイルが必要です。
データベーステーブルの作成
まず、PostgreSQLデータベースで以下のSQLステートメントを実行して、comment
テーブルを作成します。このテーブルは、post
テーブルと user
テーブルの両方にリンクされます。
CREATE TABLE "comment" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "content" TEXT NOT NULL, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "postId" UUID REFERENCES "post"("id") ON DELETE CASCADE, "userId" UUID REFERENCES "user"("id") ON DELETE CASCADE );
ON DELETE CASCADE
は、投稿またはユーザーが削除されたときに、データの一貫性を確保するために、関連するすべてのコメントも自動的に削除されることを意味します。
Leapcell でデータベースが作成された場合、
グラフィカルインターフェースを使用してSQLステートメントを簡単に実行できます。Webサイトのデータベース管理ページに移動し、上記のステートメントをSQLインターフェースに貼り付けて実行するだけです。
コメントEntityの作成
次に、コメントに関連するすべてのロジックを管理するために、comments
モジュールを作成します。
nest generate module comments nest generate service comments nest generate controller comments
src/comments
ディレクトリで、comment.entity.ts
という名前のファイルを作成します。
// src/comments/comment.entity.ts import { Entity, Column, PrimaryColumn, CreateDateColumn, ManyToOne } from 'typeorm'; import { User } from '../users/user.entity'; import { Post } from '../posts/post.entity'; @Entity() export class Comment { @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' }) id: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @ManyToOne(() => User, (user) => user.comments) user: User; @ManyToOne(() => Post, (post) => post.comments) post: Post; }
関連Entityの更新
コメントからユーザーおよび投稿への ManyToOne
リレーションシップを定義しました。次に、User
および Post
Entityで逆の OneToMany
リレーションシップを定義する必要があります。
src/users/user.entity.ts
を更新する:
// src/users/user.entity.ts import { Entity, Column, PrimaryColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Commentをインポート @Entity() export class User { @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' }) id: string; @Column({ unique: true }) username: string; @Column() password: string; @OneToMany(() => Comment, (comment) => comment.user) // リレーションシップを追加 comments: Comment[]; }
src/posts/post.entity.ts
を更新する:
// src/posts/post.entity.ts import { Entity, Column, PrimaryColumn, CreateDateColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Commentをインポート @Entity() export class Post { @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' }) id: string; @Column() title: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @OneToMany(() => Comment, (comment) => comment.post) // リレーションシップを追加 comments: Comment[]; }
ステップ2:コメントサービスの実装
次に、コメントの作成およびクエリのロジックを処理する CommentsService
を記述します。
コメントEntityの登録
src/comments/comments.module.ts
を開き、TypeOrmModule
を登録し、CommentsService
をエクスポートして、他のモジュールが使用できるようにします。
// src/comments/comments.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CommentsService } from './comments.service'; import { Comment } from './comment.entity'; import { CommentsController } from './comments.controller'; @Module({ imports: [TypeOrmModule.forFeature([Comment])], providers: [CommentsService], controllers: [CommentsController], exports: [CommentsService], // Serviceをエクスポート }) export class CommentsModule {}
サービスロジックの記述
src/comments/comments.service.ts
ファイルを変更して、コメントの作成およびクエリ用のメソッドを追加します。
// src/comments/comments.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Comment } from './comment.entity'; import { Post } from '../posts/post.entity'; import { User } from '../users/user.entity'; @Injectable() export class CommentsService { constructor( @InjectRepository(Comment) private commentsRepository: Repository<Comment> ) {} // 投稿IDごとにすべてのコメントを検索し、ユーザー情報を含める findByPostId(postId: string): Promise<Comment[]> { return this.commentsRepository.find({ where: { post: { id: postId } }, relations: ['user'], // キー:関連するユーザーオブジェクトもロードする order: { createdAt: 'ASC', // 時間の昇順でソート }, }); } // コメントを作成する async create(content: string, user: User, post: Post): Promise<Comment> { const newComment = this.commentsRepository.create({ content, user, post, }); return this.commentsRepository.save(newComment); } }
relations: ['user']
はTypeORMの非常に便利な機能です。コメントをクエリするときに、外部キーを介して関連するuser
オブジェクトを自動的に取得して設定するようにTypeORMに指示します。これにより、コメント作成者のユーザー名を簡単に取得できます。
ステップ3:コメントの送信と表示
次に、記事ページにコメント機能を統合する必要があります。
これを行うために、コメント送信を受け付ける CommentsController
にロジックを追加し、記事ページにコメントを表示するように PostsController
を更新します。
CommentsController
の実装
src/comments/comments.controller.ts
を変更します。
// src/comments/comments.controller.ts import { Controller, Post, Body, Param, Req, Res, UseGuards } from '@nestjs/common'; import { CommentsService } from './comments.service'; import { AuthenticatedGuard } from '../auth/authenticated.guard'; import type { Response, Request } from 'express'; @Controller('posts/:postId/comments') export class CommentsController { constructor(private readonly commentsService: CommentsService) {} @UseGuards(AuthenticatedGuard) // ログインユーザーのみがコメントできるようにする @Post() async create( @Param('postId') postId: string, @Body('content') content: string, @Req() req: Request, @Res() res: Response ) { // req.session.userはexpress-sessionによって追加されます const { user } = req.session as any; // 注意:実際のアプリケーションでは、postIdが存在するかどうかを確認する必要があります await this.commentsService.create(content, user, { id: postId } as any); res.redirect(`/posts/${postId}`); // コメントが正常に完了したら、記事ページにリダイレクトします } }
PostsController
の更新
PostsController
の findOne
メソッドを変更して、記事詳細ページをレンダリングするときに、その記事のすべてのコメントを取得して渡すようにします。
まず、PostsModule
が CommentsService
を使用できるように、src/posts/posts.module.ts
に CommentsModule
をインポートします。
// src/posts/posts.module.ts // ... import { CommentsModule } from '../comments/comments.module'; @Module({ imports: [TypeOrmModule.forFeature([Post]), CommentsModule], // CommentsModuleをインポート controllers: [PostsController], providers: [PostsService], }) export class PostsModule {}
次に src/posts/posts.controller.ts
を更新します。
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Request /* ... */ } from '@nestjs/common'; import { PostsService } from './posts.service'; import { CommentsService } from '../comments/comments.service'; // CommentsServiceをインポート // ... @Controller('posts') export class PostsController { constructor( private readonly postsService: PostsService, private readonly commentsService: CommentsService // CommentsServiceを注入 ) {} // ... 他のメソッドは変更なし @Get(':id') @Render('post') async post(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); const comments = await this.commentsService.findByPostId(id); // コメントを取得 return { post, user: req.session.user, comments }; // テンプレートにコメントを渡す } }
ステップ4:フロントエンドビューの更新
最後のステップは、コメントリストとコメントフォームを表示するようにEJSテンプレートを変更することです。
views/post.ejs
を開き、記事コンテンツの下に次のコードを追加します。
<a href="/" class="back-link">← Homeに戻る</a> <section class="comments-section"> <h3>コメント</h3> <div class="comment-list"> <% if (comments.length > 0) { %> <% comments.forEach(comment => { %> <div class="comment-item"> <p class="comment-content"><%= comment.content %></p> <small> 投稿者: <strong><%= comment.user.username %></strong> <%= new Date(comment.createdAt).toLocaleDateString() %> </small> </div> <% }) %> <% } else { %> <p>まだコメントがありません。最初のコメントをどうぞ!</p> <% } %> </div> <% if (user) { %> <form action="/posts/<%= post.id %>/comments" method="POST" class="comment-form"> <h4>コメントを残す</h4> <div class="form-group"> <textarea name="content" rows="4" placeholder="ここにコメントを書いてください..." required></textarea> </div> <button type="submit">コメントを送信</button> </form> <% } else { %> <p><a href="/auth/login">ログイン</a>してコメントを残してください。</p> <% } %> </section> <%- include('_footer') %>
ページをより見栄え良くするために、public/css/style.css
にスタイルを追加できます。
/* ... 他のスタイル ... */ .comments-section { margin-top: 3rem; border-top: 1px solid #eee; padding-top: 2rem; } .comment-list .comment-item { background: #f9f9f9; border: 1px solid #ddd; padding: 1rem; border-radius: 5px; margin-bottom: 1rem; } .comment-content { margin-top: 0; } .comment-item small { color: #666; } .comment-form { margin-top: 2rem; } .comment-form textarea { width: 100%; padding: 0.5rem; margin-bottom: 1rem; }
実行とテスト
アプリケーションを再起動します。
npm run start:dev
ブラウザでWebページを開きます:http://localhost:3000/
任意の記事に移動すると、コメントセクションが表示されるはずです。
コメントボックスに内容を入力して送信します。ページがリフレッシュされると、投稿したばかりのコメントがコメントリストに表示されるはずです。
おめでとうございます。ブログにコメントシステムを正常に追加しました!
もちろん、現在のコメント機能はまだ非常に基本的です。次の記事では、作成者がコメントに返信する機能を実装することで、この機能を引き続き強化し、ブログのインタラクティビティを次のレベルに引き上げます。
過去のチュートリアル: