Điều quan trọng tiếp theo khi xây dựng NestJS API là lưu trữ dữ liệu. Xây dựng và sử dụng hiểu quả database. Bộ đôi nodejs và mongodb chắc không còn quá xa lại với những bạn đã tìm hiểu về node. Để làm việc thuận tiện hơn, mình sử dụng Mongoose là một thư viện Object Data Modeling (ODM) cho MongoDB. Giúp quản lý các mối quan hệ dữ liệu giữa những collections, cung cấp schema validation và được sử dụng để translate giữa các đối tượng trong ứng dụng bao gồm code chúng ta.

Theo dõi source code tại đâybranch dev nha

Link postman để test cho dễ.

Cài đặt mongodb

Cài đặt mongodb thì cũng đơn giản thôi, chúng ta lên trang chủ của mongodb.
Tùy vào hệ điều hành mà các bạn lựa chọn cho phù hợp.

Ở máy mình đang dùng version 5.x nên các bạn cũng cài này nhe, nếu cài bản 4.4 thì trong quá trình có thể phát sinh một số lỗi do ở version này chưa hỗ trợ 1 số syntax.

Install on Windows
Download file Tại đây

Sau khi tải về, mở file lên để tiến hành cài đặt.

Chọn next

Chọn ‘I accept the terms in the License Agreement’ và next

Kiểu setup thì với các bạn chưa quen thì cứ chọn compelete để nó cài đặt tất cả các phần mềm phụ trợ

Chọn thư mục lưu data và log cho MongoDB. Cứ giữ mặc định nếu bạn không biết nó là gì

Tick chọn Install MongoDB Compass và next

Click install, sau khi cài đặt xong chọn Finish

Đã hoàn tất việc cài đặt, tới đây thì vẫn chưa chạy được đâu nha các bạn.
Chúng ta cần phải khai báo biến môi trường cho thằng mongodb này

Ta search view advanced system settings

Nếu tìm không ra thì các bạn có thể search Set Environment Variables và đi đến như thế này.
Bấm vào Environment Variables

Chọn vào Path và bấm Edit

Bấm New và add đường dẫn của MongoDB vừa cài đặt,
Thông thường sẽ có đường dẫn: C:\Program Files\MongoDB\Server\5.0\bin
Phải trỏ đến thư mục bin nha.

Mở cmd hoặc powerShell hoặc terminal và chạy lệnh mongo
Nó trả về **MongoDB shell version v5.0.4 … ** là thành công rồi nha.

Install on macOS

Mình cài thông qua brew

brew tap mongodb/brew
brew update
brew install mongodb-community@5.0
brew services start mongodb-community@5.0

Mở cmd hoặc powerShell hoặc terminal và chạy lệnh mongo
Nó trả về **MongoDB shell version v5.0.4 … ** là thành công rồi nha.

Hồi trước mình cài trên m1 pro 2021 nó bị lỗi không start được, nhưng m1 2020 thì cài bình thường. Sau khi search 7*7 49 nơi thì bằng cách thần kì gì đó nó đã hoạt động nếu bị lỗi tham khảo tại đây.

Hiện nay M1 silicon đã hỗ trợ cho thằng mongodb compass, mình cũng cài nó thông qua brew thôi

brew install --cask mongodb-compass

Cấu hình mặc định khi cài đặt Mongodb

MONGODB_URL=mongodb://localhost:27017/db_name

Rồi mình sẽ hướng dẫn các bạn nạp cái này đô ngay bên dưới.

Cài đặt mongoose và khai báo vào project

ở trên mình mới giới thiệu cái MONGODB_URL, thì cái này mình phải khai báo trong file môi trường .env

Ở node, thì ta dùng thư viện dotenv. Còn trong NestJS, chúng ta có ConfigModule viết từ thằng dotenv ra.

npm install @nestjs/config

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostModule } from './post/post.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [PostModule, ConfigModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Tạo file .env


MONGODB_URL=mongodb://localhost:27017/study_nestjs

Tiến hành cài mongose

npm install --save @nestjs/mongoose@7.2.4 mongoose@5.13.14

Kết nối ứng dụng đến CSDL

Thông thường, những thứ gì mà có tính global (tính toàn cục, sử dụng trên toàn project) thì mình sẽ khai báo trong thằng AppModule để dễ quản lý và nó hoạt động, khai báo trong các module con thì mình không đảm bảo được nó có hoạt động hay không nữa, hihii.

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostModule } from './post/post.module';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    PostModule,
    ConfigModule.forRoot(),
    MongooseModule.forRoot(process.env.MONGODB_URL, {
      useNewUrlParser: true,
      useFindAndModify: false,
      useCreateIndex: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Models

Mình xóa bỏ cái post.interface.ts ở bài trước đi nhe, cái đó chỉ là thế thân thôi, giờ mình mới làm thiệt

Giờ các bạn tạo file post.model.ts trong folder post

Trong này sẽ có 2 thành phần, 1 là schema để khởi tạo cấu trúc cho table. Phần còn lại là interface, đại diện cho table đó.

post.model.ts

import { Schema, Document } from 'mongoose';

const PostSchema = new Schema(
  {
    title: String,
    description: String,
    content: String,
  },
  {
    timestamps: true,
    // timestamps: {
    //   createdAt: 'created_at',
    //   updatedAt: 'updated_at',
    // },
    collection: 'posts',
  },

    // created_at: { type: Date, required: true, default: Date.now },
);

export { PostSchema };

export interface Post extends Document {
  title: string;
  description: string;
  content: string;
}

Mình đó để một số option để các bạn lựa chọn, như timestamps hay collection, collection kiểu như là tên của table bên sql vậy, các bạn có thể tùy chỉnh, còn không có vẫn lấy giá trị mặc định theo schema name ở dạng số nhiều.

Tới đây thì vẫn chưa hoạt động được đâu, chúng ta cần khai báo cái PostSchema này cho thằng mongo hiểu.

Lưu ý: Chúng ta không sử dụng id cho khóa chính mà sử dụng _id và chúng ta không cần khai báo thằng này trong interface, một số câu truy vấn, mặc định nó sẽ trả về trường id này, nhưng những câu truy vấn chuyên sâu hơn lại không có, nên việc sử dụng trường id rất bất cập. Và hãy sử dụng _id.

post.module.ts

import { Module } from '@nestjs/common';
import { PostController } from './post.controller';
import { PostService } from './post.service';
import { MongooseModule } from '@nestjs/mongoose';
import { PostSchema } from './post.model';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: 'Post',
        schema: PostSchema,
      },
    ]),
  ],
  controllers: [PostController],
  providers: [PostService],
})
export class PostModule {}

Cái name: 'Post' khá là quan trọng đó, nó kiểu như alias, định danh cho thằng schema này, chớ thằng PostModel nó chỉ là interface mà thôi.

Như mình đã giới thiệu, thì chúng ta sẽ làm việc với database thông qua thằng Repository.

Có phải 1 repository sẽ có những phương thức CRUD chung, giờ không lẽ cái repo nào mình cũng tạo hết thì mất công, vậy nên ta sẽ tạo 1 cái base repositoty để cho những thằng khác kế thừa lại thôi.

Ta sẽ tạo thằng base.repository.ts nằm cùng tầng với thằng app module, vì nó dùng cho tất cả các module mà.ư

base.repository.ts

import { Model, FilterQuery, QueryOptions, Document } from 'mongoose';

export class BaseRepository<T extends Document> {
  constructor(private readonly model: Model<T>) {}

  async create(doc): Promise<any> {
    const createdEntity = new this.model(doc);
    return await createdEntity.save();
  }

  async findById(id: string, option?: QueryOptions): Promise<T> {
    return this.model.findById(id, option);
  }

  async findByCondition(
    filter,
    field?: any | null,
    option?: any | null,
    populate?: any | null,
  ): Promise<T> {
    return this.model.findOne(filter, field, option).populate(populate);
  }

  async getByCondition(
    filter,
    field?: any | null,
    option?: any | null,
    populate?: any | null,
  ): Promise<T[]> {
    return this.model.find(filter, field, option).populate(populate);
  }

  async findAll(): Promise<T[]> {
    return this.model.find();
  }

  async aggregate(option: any) {
    return this.model.aggregate(option);
  }

  async populate(result: T[], option: any) {
    return await this.model.populate(result, option);
  }

  async deleteOne(id: string) {
    return this.model.deleteOne({ _id: id } as FilterQuery<T>);
  }

  async deleteMany(id: string[]) {
    return this.model.deleteMany({ _id: { $in: id } } as FilterQuery<T>);
  }

  async deleteByCondition(filter) {
    return this.model.deleteMany(filter);
  }

  async findByConditionAndUpdate(filter, update) {
    return this.model.findOneAndUpdate(filter as FilterQuery<T>, update);
  }

  async updateMany(filter, update, option?: any | null, callback?: any | null) {
    return this.model.updateMany(filter, update, option, callback);
  }

  async findByIdAndUpdate(id, update) {
    return this.model.findByIdAndUpdate(id, update);
  }
}

Lưu ý: các bạn nhớ cài đúng version nếu không sẽ có một số lỗi không mong muốn xảy ra. Do mình đang dùng @nestjs/mongoose@7.2.4 mongoose@5.13.14 nên sẽ hướng dẫn trên cái này.

Bây giờ ta mới tạo file post.repository.ts trong folder post

post.repository.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Post } from './post.model';
import { BaseRepository } from '../base.repository';

@Injectable()
export class PostRepository extends BaseRepository<Post> {
  constructor(
    @InjectModel('Post')
    private readonly postModel: Model<Post>,
  ) {
    super(postModel);
  }
}

Cái @InjectModel(‘Post’) chính là cái name schema mình khai báo bên trong PostModule á. Thằng PostRepository có tất cả phương thức mà thằng BaseRepository khai báo, và muốn viết gì thêm vào bên trong này vẫn được nha.

Cuối cùng là khai báo nó vào phần providers của PostModule

import { PostRepository } from './post.repository';
...

  providers: [PostService, PostRepository],

...

và sử dụng nó trong thằng PostService

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreatePostDto, UpdatePostDto } from './dto/post.dto';
import { PostRepository } from './post.repository';

@Injectable()
export class PostService {
  constructor(private readonly postRepository: PostRepository) {}

  async getAllPosts() {
    return this.postRepository.getByCondition({});
  }

  async getPostById(post_id: string) {
    const post = this.postRepository.findById(post_id);
    if (post) {
      return post;
    }
    throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
  }

  async replacePost(post_id: string, data: UpdatePostDto) {
    return await this.postRepository.findByIdAndUpdate(post_id, data);
  }

  async createPost(post: CreatePostDto) {
    return await this.postRepository.create(post);
  }

  async deletePost(post_id: string) {
    return await this.postRepository.deleteOne(post_id);
  }
}

Cập nhật lại file post.dto.ts. Ta sử dụng thư viện class-validator để validate data gửi lên của người dùng.

npm install class-validator --save

post.dto.ts

import { IsNotEmpty } from 'class-validator';

export class CreatePostDto {
  @IsNotEmpty() title: string;
  description: string;
  content: string;
}

export class UpdatePostDto {
  @IsNotEmpty() title: string;
  description: string;
  content: string;
}

Các bạn có thể theo dõi thêm các hàm validate tại repo git của nó tại đây

Kết luận

Đây là những kiến thức mình đã làm ở các project khác và nó đã hoạt động trơn tru nhe. NestJS hay NodeJS có rất nhiều kiểu khai báo khác nhau. Đôi khi bạn đọc một số tài liệu khác họ lại chỉ khác thì cũng đừng thắc mắc nhe. Miễn sao nó không bị lỗi là được nè.

Nguồn tham khảo:

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!