Nest.js Blog Step by Step: Comment System
Takashi Yamamoto
Infrastructure Engineer · Leapcell

In the previous tutorial, we integrated a complete user authentication system for our blog based on Sessions and Passport.js. Now, users can register and log in to the blog system, and we have protected routes that require a login to access.
Since we can now clearly distinguish between readers and authors, isn't it the perfect time to add a feature for interaction between them?
In this article, we will add a fundamental yet core feature to our blog: a commenting system.
Specifically, we will implement the following functionalities:
- Display a list of comments below each post.
- Allow logged-in users to post comments.
Step One: Create the Data Model for Comments
Just like our Post
and User
entities, our Comment
entity needs its own database table and a corresponding entity file.
1. Create the Database Table
First, execute the following SQL statement in your PostgreSQL database to create the comment
table. This table establishes relationships with the post
and user
tables via postId
and userId
.
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 );
Tip:
ON DELETE CASCADE
means that when aPost
or aUser
is deleted, all of their associated comments will also be automatically deleted to ensure data consistency.
2. Create the Comment Entity
Next, let's use the Nest CLI to create a new comments
module to manage all comment-related logic.
nest generate module comments nest generate service comments
For now, we don't need to create a separate controller for comments, as submitting and displaying comments will be attached to posts. Therefore, we'll place the relevant routing logic in the PostsController
and a CommentsController
we'll create later.
Create a file named comment.entity.ts
in the src/comments
directory:
// src/comments/comment.entity.ts import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, ManyToOne } from 'typeorm'; import { User } from '../users/user.entity'; import { Post } from '../posts/post.entity'; @Entity() export class Comment { @PrimaryGeneratedColumn('uuid') id: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @ManyToOne(() => User, (user) => user.comments) user: User; @ManyToOne(() => Post, (post) => post.comments) post: Post; }
3. Update the Related Entities
We have defined the ManyToOne
relationship from comments to users and posts. Now, we need to define the reverse OneToMany
relationship in the User
and Post
entities.
Update src/users/user.entity.ts
:
// src/users/user.entity.ts import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Import Comment @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) username: string; @Column() password: string; @OneToMany(() => Comment, (comment) => comment.user) // Add the relationship comments: Comment[]; }
Update src/posts/post.entity.ts
:
// src/posts/post.entity.ts import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Import Comment @Entity() export class Post { @PrimaryGeneratedColumn('uuid') id: string; @Column() title: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @OneToMany(() => Comment, (comment) => comment.post) // Add the relationship comments: Comment[]; }
Step Two: Implement the Comment Service
Now let's write the CommentsService
, which will be responsible for handling the logic for creating and querying comments.
1. Register the Comment Entity
Open src/comments/comments.module.ts
, register TypeOrmModule
, and export the CommentsService
so that other modules can use it.
// 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'; @Module({ imports: [TypeOrmModule.forFeature([Comment])], providers: [CommentsService], exports: [CommentsService], // Export the service }) export class CommentsModule {}
2. Write the Service Logic
Modify the src/comments/comments.service.ts
file to add methods for creating and querying comments.
// 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> ) {} // Find all comments for a post by its ID, including user information findByPostId(postId: string): Promise<Comment[]> { return this.commentsRepository.find({ where: { post: { id: postId } }, relations: ['user'], // Key: Also load the associated user object order: { createdAt: 'ASC', // Sort by creation time in ascending order }, }); } // Create a comment 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']
is a powerful feature of TypeORM. It tells TypeORM to automatically fetch and populate the associateduser
object via the foreign key when querying for comments. This allows us to easily get the username of the comment's author.
Step Three: Submit and Display Comments
Now, we need to integrate the comment functionality into the post page. This includes creating a controller to handle comment submissions and updating the PostsController
to display comments on the post page.
1. Create the CommentsController
Although we could handle comment submissions in the PostsController
, we'll create a dedicated controller for it to maintain a separation of concerns.
nest generate controller comments
Modify 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 { Response, Request } from 'express'; @Controller('posts/:postId/comments') export class CommentsController { constructor(private readonly commentsService: CommentsService) {} @UseGuards(AuthenticatedGuard) // Ensure only logged-in users can comment @Post() async create( @Param('postId') postId: string, @Body('content') content: string, @Req() req: Request, @Res() res: Response ) { // req.user is added by the Passport session const user = req.user as any; // Note: In a real application, you should validate that the postId exists await this.commentsService.create(content, user, { id: postId } as any); res.redirect(`/posts/${postId}`); // After commenting, redirect back to the post page } }
Also, don't forget to register this new controller in src/comments/comments.module.ts
.
// src/comments/comments.module.ts // ... import { CommentsController } from './comments.controller'; @Module({ imports: [TypeOrmModule.forFeature([Comment])], controllers: [CommentsController], // Add the controller providers: [CommentsService], exports: [CommentsService], }) export class CommentsModule {}
2. Update the PostsController
We need to modify the findOne
method of the PostsController
so that when it renders the post detail page, it also fetches and passes all the comments for that post.
First, import CommentsModule
into src/posts/posts.module.ts
so that PostsModule
can use CommentsService
.
// src/posts/posts.module.ts // ... import { CommentsModule } from '../comments/comments.module'; @Module({ imports: [TypeOrmModule.forFeature([Post]), CommentsModule], // Import CommentsModule controllers: [PostsController], providers: [PostsService], }) export class PostsModule {}
Then, update 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'; // Import CommentsService // ... @Controller('posts') export class PostsController { constructor( private readonly postsService: PostsService, private readonly commentsService: CommentsService // Inject CommentsService ) {} // ... other methods remain unchanged @Get(':id') @Render('post') async findOne(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); const comments = await this.commentsService.findByPostId(id); // Get comments return { post, user: req.user, comments }; // Pass comments to the template } }
Step Four: Update the Frontend View
The final step is to modify the EJS template to display the comment list and the comment form.
Open views/post.ejs
and add the following code below the post content:
<a href="/" class="back-link">← Back to Home</a> <section class="comments-section"> <h3>Comments</h3> <div class="comment-list"> <% if (comments.length > 0) { %> <% comments.forEach(comment => { %> <div class="comment-item"> <p class="comment-content"><%= comment.content %></p> <small> By <strong><%= comment.user.username %></strong> on <%= new Date(comment.createdAt).toLocaleDateString() %> </small> </div> <% }) %> <% } else { %> <p>No comments yet. Be the first to comment!</p> <% } %> </div> <% if (user) { %> <form action="/posts/<%= post.id %>/comments" method="POST" class="comment-form"> <h4>Leave a Comment</h4> <div class="form-group"> <textarea name="content" rows="4" placeholder="Write your comment here..." required ></textarea> </div> <button type="submit">Submit Comment</button> </form> <% } else { %> <p><a href="/auth/login">Login</a> to leave a comment.</p> <% } %> </section> <%- include('_footer') %>
To make the page look better, you can add some styles to public/css/style.css
:
/* ... other styles ... */ .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; }
Run and Test
Restart your application:
npm run start:dev
Now, open your browser:
- Visit any post as a guest. You will see the comments section, but only a "Login to leave a comment" link.
- Log in to your account.
- Visit the same post again. You will now see a comment box where you can input text.
- Submit a comment. After the page refreshes, you will see the content you just posted in the comment list.
Congratulations! You have successfully added a fully functional commenting system to your Nest.js blog. We created the data model for comments, implemented the backend service and routing logic, and updated the frontend view to display comments and a submission form, while using a Guard to ensure that only logged-in users can comment.
Of course, the current comment feature is still quite basic. In the next article, we will continue to enhance this feature by implementing logic for authors to reply to comments, taking the interactivity of the blog to the next level.