Cách tích hợp MongoDB với NestJS sử dụng Mongoose

By Thái Nguyễn
Picture of the author
Published on
nestjs and mongoose

MongoDB là một cơ sở dữ liệu NoSQL phổ biến, và kết hợp nó với NestJS giúp xây dựng các ứng dụng nhanh chóng và hiệu quả. Trong bài viết này, chúng ta sẽ tìm hiểu cách tích hợp MongoDB vào một dự án NestJS sử dụng Mongoose.

Nội dung

Cấu trúc của project demo

Dưới đây là cấu trúc thư mục của project demo, giúp tổ chức mã nguồn một cách rõ ràng:

nest-mongodb-demo/
├── src/
│   ├── modules/
│   │   └── articles/              # Demo module
│   │       ├── dto/
│   │       │   ├── create-article.dto.ts
│   │       │   ├── search-article.dto.ts
│   │       │   └── update-article.dto.ts
│   │       ├── schemas/
│   │       │   └── article.schema.ts
│   │       ├── articles.controller.ts
│   │       ├── articles.service.ts
│   │       └── articles.module.ts
│   ├── app.module.ts
│   └── main.ts
├── .env                          # MongoDB connection config
└── package.json

Lưu ý:

MongoDB: Cơ sở dữ liệu NoSQL phổ biến.

Mongoose: Thư viện ODM (Object Data Modeling) cho MongoDB tương thích tốt với NestJS.

1. Cài Đặt và Cấu Hình

1.1. Cài đặt các package cần thiết Để sử dụng MongoDB trong NestJS, bạn cần cài đặt thư viện @nestjs/mongoose và mongoose:

npm install @nestjs/mongoose mongoose

1.2. Tạo file .env và cấu hình MongoDB Tạo một file .env ở thư mục gốc của dự án để lưu thông tin kết nối:

MONGODB_URI=mongodb://localhost:27017/nest-mongodb-demo

1.3. Cấu hình MongoDB trong AppModule Bạn cần chỉnh sửa file app.module.ts để sử dụng MongooseModule và đọc thông tin từ file .env:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { ArticlesModule } from './modules/articles/articles.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [`.env.${process.env.NODE_ENV || 'dev'}`, '.env'],
    }),
    MongooseModule.forRootAsync({
      useFactory: async () => ({
        uri: process.env.MONGODB_URI,
      }),
    }),
    ArticlesModule,
  ],
})
export class AppModule {}

2. Định Nghĩa Schema

Schema xác định cấu trúc tài liệu trong MongoDB. NestJS sử dụng các decorator để định nghĩa schema một cách rõ ràng.

Ví dụ Schema cho Article

// src/modules/articles/schemas/article.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
import { Schema as MongooseSchema } from 'mongoose';
import { Category } from './category.schema'; // Giả sử bạn có category schema riêng

@Schema()
export class Article {
  @Prop({ required: true, maxlength: 255 })
  title: string;

  @Prop({ required: true })
  content: string;

  @Prop({ maxlength: 1000 })
  description: string;

  @Prop()
  thumbnail: string;

  @Prop({ default: 0 })
  numberOfView: number;

  @Prop({
    type: [{ type: MongooseSchema.Types.ObjectId, ref: 'Category' }],
  })
  categories: Category[];
}

export type ArticleDocument = HydratedDocument<Article>;
export const ArticleSchema = SchemaFactory.createForClass(Article);

Giải thích:

  • @Schema(): Đánh dấu một class là schema của MongoDB.
  • @Prop(): Định nghĩa một thuộc tính của tài liệu.
  • SchemaFactory.createForClass: Tạo schema dựa trên class.

3. Tạo Module

Mỗi tài nguyên trong ứng dụng NestJS thường được quản lý bởi một module.

// src/modules/articles/articles.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ArticlesController } from './articles.controller';
import { ArticlesService } from './articles.service';
import { Article, ArticleSchema } from './schemas/article.schema';

@Module({
  imports: [
    MongooseModule.forFeature([
      { name: Article.name, schema: ArticleSchema },
    ]),
  ],
  controllers: [ArticlesController],
  providers: [ArticlesService],
  exports: [ArticlesService],
})
export class ArticlesModule {}

Lưu ý:

MongooseModule.forFeature: Đăng ký schema để sử dụng trong module này.

4. Implement Service

Service chịu trách nhiệm xử lý logic của ứng dụng và giao tiếp với MongoDB thông qua model.

4.1. CRUD Operations
// src/modules/articles/articles.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Article, ArticleDocument } from './schemas/article.schema';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { SearchArticleDto } from './dto/search-article.dto';

@Injectable()
export class ArticlesService {
  constructor(
    @InjectModel(Article.name) private articleModel: Model<ArticleDocument>,
  ) {}

  // Create
  async create(createArticleDto: CreateArticleDto): Promise<Article> {
    const createdArticle = new this.articleModel(createArticleDto);
    return createdArticle.save();
  }

  // Read
  async findAll(searchDto: SearchArticleDto): Promise<Article[]> {
    // Ví dụ bạn có thể tùy biến prepareQuery(searchDto)
    const query: any = {};
    if (searchDto.title) {
      query.title = { $regex: searchDto.title, $options: 'i' };
    }

    return this.articleModel
      .find(query)
      .sort({ _id: -1 })
      .populate('categories')
      .exec();
  }

  // Update
  async update(id: string, updateArticleDto: UpdateArticleDto): Promise<Article> {
    return this.articleModel
      .findByIdAndUpdate(id, updateArticleDto, { new: true })
      .exec();
  }

  // Delete
  async delete(id: string): Promise<Article> {
    return this.articleModel.findByIdAndDelete(id).exec();
  }
}

Giải thích:

  • InjectModel: Inject model để thao tác với MongoDB.
  • create, findAll, update, delete: Các phương thức cơ bản để thao tác với MongoDB.
5. Implement Controller

Controller chịu trách nhiệm nhận yêu cầu từ client và chuyển chúng đến service để xử lý.

// src/modules/articles/articles.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { SearchArticleDto } from './dto/search-article.dto';
import { Article } from './schemas/article.schema';

@Controller('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Post()
  async create(@Body() createArticleDto: CreateArticleDto): Promise<Article> {
    return this.articlesService.create(createArticleDto);
  }

  @Get()
  async findAll(@Query() searchDto: SearchArticleDto): Promise<Article[]> {
    return this.articlesService.findAll(searchDto);
  }

  @Put(':id')
  async update(
    @Param('id') id: string,
    @Body() updateArticleDto: UpdateArticleDto,
  ): Promise<Article> {
    return this.articlesService.update(id, updateArticleDto);
  }

  @Delete(':id')
  async delete(@Param('id') id: string): Promise<Article> {
    return this.articlesService.delete(id);
  }
}

6. Data Transfer Objects (DTOs)

DTOs được sử dụng để validate dữ liệu đầu vào và đầu ra. Sử dụng thư viện class-validator và class-transformer để validate dữ liệu.

// src/modules/articles/dto/create-article.dto.ts
import { IsNotEmpty, IsString, MaxLength, IsOptional, IsArray } from 'class-validator';

export class CreateArticleDto {
  @IsNotEmpty()
  @IsString()
  @MaxLength(255)
  title: string;

  @IsNotEmpty()
  @IsString()
  content: string;

  @IsOptional()
  @IsString()
  @MaxLength(1000)
  description?: string;

  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  categories?: string[];
}

7. Relationships và Populate

Mongoose hỗ trợ các loại relationships khác nhau, bao gồm one-to-many, many-to-many, và one-to-one. Dưới đây là một ví dụ về one-to-many relationship.

7.1. One-to-Many Relationship
// src/modules/articles/schemas/article.schema.ts

@Prop({
  type: [{ type: MongooseSchema.Types.ObjectId, ref: 'Category' }],
})
categories: Category[];
// src/modules/articles/articles.service.ts

const article = await this.articleModel
  .findById(id)
  .populate('categories')
  .exec();

Populate một số trường của schema khác

// src/modules/articles/articles.service.ts

const article = await this.articleModel
  .findById(id)
  .populate({
    path: 'categories',
    select: 'name description',
  })
  .exec();

8. Best Practices

Một số best practices khi sử dụng Mongoose với NestJS:

  1. Sử dụng TypeScript Decorators
  • Leverage @Prop(), @Schema() cho MongoDB schemas
  • Sử dụng validation decorators cho DTOs
  1. Implement Pagination
  • Luôn có limit và skip cho queries lớn
  • Trả về metadata với kết quả
  1. Error Handling
  • Sử dụng custom exceptions
  • Implement global exception filters
  1. Type Safety
  • Định nghĩa interfaces và types
  • Sử dụng DTOs cho data validation
  1. Performance
  • Index các fields thường query
  • Sử dụng projection để select specific fields
  • Implement caching khi cần thiết

Tổng kết

Bài viết đã giới thiệu cách tích hợp MongoDB vào một dự án NestJS, bao gồm:

  • Cài đặt và cấu hình Mongoose
  • Định nghĩa schema
  • Tạo module, implement service và controller
  • Sử dụng DTOs để validate dữ liệu
  • Sử dụng relationships, populate
  • Và một số “Best Practices” quan trọng

Hy vọng với hướng dẫn này, bạn có thể nhanh chóng xây dựng và triển khai các tính năng phức tạp hơn trong dự án NestJS + MongoDB. Chúc bạn thành công!