DEV Community

Cover image for Build a Great Nest.js Blog: Tags for Post
Leapcell
Leapcell

Posted on

Build a Great Nest.js Blog: Tags for Post

In the previous tutorial, we added visitor tracking to our blog, allowing us to visually see how popular each article is.

The blog already looks quite complete, but it seems like something is still missing. Your blog already has a lot of articles, and users might get lost in them... So, how can users quickly find the topics they are interested in reading?

That's right, the blog now needs a tag feature.

Tags are used to display the theme and content of an article. You can assign multiple keywords to an article (e.g., "Technical Tutorial," "Nest.js," "Database").

In the next two tutorials, we will add a tag feature to our blog system. In this tutorial, we will first implement support for setting tags when creating and editing articles.

Step 1: Data Modeling and Relationship Building

Create the Tag Entity

First, let's create a corresponding module and entity for this new concept.

nest generate module tags
nest generate service tags
Enter fullscreen mode Exit fullscreen mode

Create tag.entity.ts in the src/tags directory:

// src/tags/tag.entity.ts
import { Entity, Column, PrimaryColumn, ManyToMany } from 'typeorm';
import { Post } from '../posts/post.entity';

@Entity()
export class Tag {
  @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' })
  id: string;

  @Column({ unique: true })
  name: string;

  @ManyToMany(() => Post, (post) => post.tags)
  posts: Post[];
}
Enter fullscreen mode Exit fullscreen mode

Update the Post Entity to Establish the Relationship

Next, we need to update the src/posts/post.entity.ts file to establish the association between posts and tags.

A post can have multiple tags, and a tag can be associated with multiple posts. This is a Many-to-Many relationship.

// src/posts/post.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
import { Tag } from '../tags/tag.entity';
// ... other imports

@Entity()
export class Post {
  // ... other fields (id, title, content, etc.)

  @ManyToMany(() => Tag, (tag) => tag.posts, {
    cascade: true, // Allows creating new tags via a post
  })
  @JoinTable() // @JoinTable must be specified on one side of the relation
  tags: Tag[];

  // ... other relations (comments, views, etc.)
}
Enter fullscreen mode Exit fullscreen mode

@JoinTable() is a decorator required to define a many-to-many relationship. You need to create a join table post_tags_tag to manage the association between Post and Tag.

Update the Database Table Structure

Execute the following SQL statements to create the new tables and fields.

-- Create the tag table
CREATE TABLE "tag" (
    "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    "name" VARCHAR UNIQUE NOT NULL
);

-- Create the post_tags_tag join table
CREATE TABLE "post_tags_tag" (
    "postId" UUID REFERENCES "post" ON DELETE CASCADE,
    "tagId" UUID REFERENCES "tag" ON DELETE CASCADE,
    PRIMARY KEY ("postId", "tagId")
);
Enter fullscreen mode Exit fullscreen mode

If your database was created on Leapcell,

Leapcell

you can easily execute SQL statements using the graphical interface. Just go to the Database management page on the website, paste the above statements into the SQL interface, and execute them.

ImageP0

Step 2: Implementing Backend Logic

We need to write a Service to handle the creation and finding of tags and update the PostsService to handle these associations when creating a post.

Writing the TagsService

The logic for this service is relatively simple, mainly focusing on finding or creating tags.

Open src/tags/tags.module.ts and register TypeOrmModule.

// src/tags/tags.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Tag } from './tag.entity';
import { TagsService } from './tags.service';

@Module({
  imports: [TypeOrmModule.forFeature([Tag])],
  providers: [TagsService],
  exports: [TagsService], // Export the service for other modules to use
})
export class TagsModule {}
Enter fullscreen mode Exit fullscreen mode

In src/tags/tags.service.ts:

// src/tags/tags.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { Tag } from './tag.entity';

@Injectable()
export class TagsService {
  constructor(
    @InjectRepository(Tag)
    private tagsRepository: Repository<Tag>
  ) {}

  // Find or create tags
  async findOrCreate(tagNames: string[]): Promise<Tag[]> {
    const existingTags = await this.tagsRepository.findBy({ name: In(tagNames) });
    const existingTagNames = existingTags.map((tag) => tag.name);

    const newTagNames = tagNames.filter((name) => !existingTagNames.includes(name));

    const newTags = newTagNames.map((name) => this.tagsRepository.create({ name }));
    await this.tagsRepository.save(newTags);

    return [...existingTags, ...newTags];
  }
}
Enter fullscreen mode Exit fullscreen mode

Update PostsService to Handle the Association

We need to modify the create and findOne methods.

First, import TagsModule into PostsModule.

// src/posts/posts.module.ts
import { TagsModule } from '../tags/tags.module';

@Module({
  imports: [TypeOrmModule.forFeature([Post]), CommentsModule, TrackingModule, TagsModule],
  // ...
})
export class PostsModule {}
Enter fullscreen mode Exit fullscreen mode

Then update src/posts/posts.service.ts:

// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { TagsService } from '../tags/tags.service';
// ... other imports

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository<Post>,
    private readonly tagsService: TagsService // Inject TagsService
  ) {}

  // Update the create method
  async create(post: Omit<Partial<Post>, 'tags'> & { tags: string }): Promise<Post> {
    const tagNames = post.tags
      .split(',')
      .map((tag) => tag.trim())
      .filter(Boolean);
    const tags = await this.tagsService.findOrCreate(tagNames);

    const newPost = this.postsRepository.create({
      ...post,
      tags,
    });

    return this.postsRepository.save(newPost);
  }

  // Update the findOne method to load related data
  findOne(id: string): Promise<Post | null> {
    return this.postsRepository.findOne({
      where: { id },
      relations: ['tags'], // Load tags
    });
  }

  // ... other methods
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Frontend Page Integration

The final step is to modify the EJS templates to support setting tags when creating and editing posts, and to display tags on the post detail page.

Update the New/Edit Post Page

Open views/new-post.ejs and add a form field for entering tags.

<form action="/posts" method="POST" class="post-form">
  <div class="form-group">
    <label for="tags">Tags (comma-separated)</label>
    <input type="text" id="tags" name="tags" />
  </div>

  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

For simplicity, we are using comma-separated input for multiple tags here. In a real-world project, you could use more complex UI components and logic, such as a dedicated tag input component, auto-matching existing tags, etc., to improve the user experience.

Update the Post Detail Page

Open views/post.ejs and display the tags in the post's metadata.

<article class="post-detail">
  <h1><%= post.title %></h1>
  <small> <%= new Date(post.createdAt).toLocaleDateString() %> | Views: <%= viewCount %> </small>

  <div class="post-content"><%- post.content %></div>

  <% if (post.tags && post.tags.length > 0) { %>
  <div class="tags-section">
    <strong>Tags:</strong>
    <% post.tags.forEach(tag => { %>
    <a href="/tags/<%= tag.id %>" class="tag-item"><%= tag.name %></a>
    <% }) %>
  </div>
  <% } %>
</article>
Enter fullscreen mode Exit fullscreen mode

Running and Testing

Restart your application:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Open your browser and go to: http://localhost:3000/

Create a new post, and you will see the tag input box at the bottom.

Enter some tags, separated by commas, for example, Nest.js, Tutorial, and then submit.

ImageP1

After submitting, go to the post detail page, and you will see that the post's tags are now displayed successfully.

ImageP2

Your blog now supports the creation and display of tags. However, users still can't filter articles by tags. We will implement this feature in the next tutorial.


Previous Tutorials:


Follow us on X: @LeapcellHQ


Read on our blog

Related Posts:

Top comments (0)