+3

Học nestjs căn bản qua góc nhìn của Dev ROR

Giới thiệu

Mình là ai ?

  • Mình là một dev Backend Ruby, framework hay dùng là RAILS (ROR). Đã có khá nhiều năm làm việc với ROR, thỉnh thoảng cũng có dùng một vài ngôn ngữ khác như python, nodejs với puppeteer,... cho các tool nho nhỏ của mình, tuy nhiên chỉ dừng lại là các tool chạy được và không theo quy tắc viết nào cả. Ngoài ra thì công việc chủ yếu là làm backend (xử lý logic, API, query ...), do đó cũng ít khi dùng đến javascript, nếu có chủ yếu là Jquery, ajax cơ bản.
  • Với xu thế chung của thị trường, để đáp ứng được nhiều yêu cầu của các dự án mới, mình chọn học NESTJS. Với profile như trên thì ngay khi tiếp cận với Nestjs, quan điểm của mình là sẽ cố gắng tìm những điểm giống nhau, những thứ cơ bản bên NESTJS thì bên ROR có gì, để từ đó dễ hiểu hơn về nó.

Nestjs là gì ?

  • NestJS là một framework Node. js mã nguồn mở, có thể mở rộng, linh hoạt, tiến bộ để xây dựng ứng dụng phía server. Nó sử dụng TypeScript và rất linh hoạt để xây dựng các hệ thống phụ trợ hấp dẫn và đòi hỏi nhiều yêu cầu. Ngoài ra, NestJS còn hỗ trợ các cơ sở dữ liệu như MongoDB, MySQL, ... document sử dụng: https://docs.nestjs.com/
  • Một vài khái niệm cơ bản sẽ được đề cập trong bài viết này: controller, module, service, provider, middleware, guards, ioc, inject, respository...

Controller, Module, Service, Provider, Reponsitory

Luồng quan hệ và khái niệm IOC

Vì chi tiết của các khái niệm trên đã được nestjs đề cập khá kỹ, nên mình sẽ không viết lại trong bài viết, tuy nhiên mình sẽ nhắc lại chúng dưới góc nhìn trực quan hơn.

  • Controller, nơi tiếp nhận request, XỬ LÝ và response cho Client (tại sao lại viết hoa chữ xử lý, mình sẽ giải thích ở phía dưới đây).
  • Provider là một khái niệm tổng quát trong NestJS và bao gồm cả services, controllers, factories, và các thành phần khác.
  • Service, nơi cung cấp dịch vụ để làm nhiệm vụ XỬ LÝ logic mà controller yêu cầu, service cũng là 1 loại provider
  • Module, được thiết kế để đóng gói liên quan đến một chức năng độc lập, dùng để mô tả các thuộc tính như controller, provider hay dependency của module này.

Mình sẽ viết lại ví dụ trong nestjs doc ra đây:

// cats.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  private readonly cats: string[] = [];

  create(cat: string) {
    this.cats.push(cat);
  }

  findAll(): string[] {
    return this.cats;
  }
}
// cats.controller.ts

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  create(@Body() catDto: { name: string }) {
    this.catsService.create(catDto.name);
  }

  @Get()
  findAll(): string[] {
    return this.catsService.findAll();
  }
}
// cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}
// app.module.ts

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module'; // Import CatsModule
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [CatsModule], // Import CatsModule vào AppModule
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Thứ tự viết lại từ service, đến controller, đến catsModule rồi đến AppModule gần giống với luồng viết code mà bạn cần xử lý trong dự án. Dưới góc nhìn đời thường thì chúng ta hiểu như sau:

  • Controller là một cơ quan tiếp nhận, xử lý, phản hồi hồ sơ cho người dân.
  • Controller hầu hết sẽ sử dụng các Service, hay gọi là các phòng ban để xử lý các yêu cầu cụ thể. Ông service này có trách nhiệm đưa ra kết quả cho ô Controller xử lý tiếp. do đó trong controller phải khai báo service
// cats.controller.ts
...
constructor(private readonly catsService: CatsService) {}
...

(ở phiên bản cũ hơn NestJS 7, chúng ta sẽ sử dụng @Inject() để chèn các phụ thuộc vào controller => cái này gần giống include Helper trong ROR)

@Inject(CatsService) private readonly catsService: CatsService,
  • CatsModule cơ quan có thẩm quyền cao hơn nhiệm vụ là khai báo ông Controller và các service của ông ý đang dùng lên cơ quan có thẩm quyền cao hơn là NESTJS (một kiểu bổ nhiệm vị trí). Và tất nhiên trên ông CatsModule thì có một ông tổng module là AppModule. Do đó mà ta thấy trong CatsModule phải khai báo đầy đủ CatsService, CatsController (mang tính đóng gói). Còn AppModule thì chỉ cần có khai báo CatsModule là được.

  • Quan hệ giữa các thành phần này (Service, Module, Controller) và cách chúng tương tác với nhau như trên dựa trên nguyên tắc IOC (Inversion of Control), cho phép bạn đảo ngược quá trình kiểm soát (control) và quản lý phụ thuộc từ phía ứng dụng sang framework (NestJS). Điều này giúp tăng tính linh hoạt, kiểm tra, và quản lý trong việc phát triển ứng dụng.

Hình như chúng ta quên nhắc đến Reponsitory ?

Responsity trong Nestjs nó gần giống như Model trong ROR. Chúng đều đại diện cho một lớp, tầng chịu trách nhiệm thực hiện các hoạt động cơ bản liên quan đến cơ sở dữ liệu. Tuy nhiên điểm khác nhau là:

Repository trong NestJS:

  • Chúng không bắt buộc phải tồn tại trong một ORM cụ thể và có thể được triển khai bằng cách sử dụng truy vấn SQL trực tiếp hoặc các thư viện tương tác với cơ sở dữ liệu khác.
  • Repository thường sử dụng để tách biệt logic liên quan đến cơ sở dữ liệu khỏi các thành phần khác của ứng dụng.

Model trong Ruby on Rails:

  • Model trong Ruby on Rails thường liên kết chặt chẽ với cơ sở dữ liệu thông qua ORM (chẳng hạn như ActiveRecord).
  • Model đại diện cho một bảng trong cơ sở dữ liệu và định nghĩa cấu trúc dữ liệu của bảng đó.
  • Ruby on Rails cung cấp các công cụ để tạo, đọc, cập nhật và xóa bản ghi trong cơ sở dữ liệu một cách dễ dàng thông qua Model. Nhắc đến đây thì có vẻ là chúng ta mường tượng được Reponsitory là gì và dùng thế nào rồi đúng không ? Cách khai báo và sử dụng Reponsitory trong nestjs và sử dụng như sau:

cats.controller.ts:

...
@Controller('cats')
export class CatsController {
  constructor(
    @InjectRepository(Cat) // Inject repository cho entity Cat
    private readonly catRepository: Repository<Cat>,
  ) {}

 @Get()
  async findAll(): Promise<Cat[]> {
    return await this.catRepository.find(); // Sử dụng repository để truy vấn dữ liệu
  }
}

cats.module.ts:

...
@Module({
  imports: [TypeOrmModule.forFeature([Cat])], // Đăng ký entity và repository
  controllers: [CatsController],
})
export class CatsModule {}

Share service thông qua export module

Để ở các module có thể sử dụng Service ở trong module khác chúng ta có thể export nó trong các module như sau: cats.module.ts

...
@Module({
  imports: [TypeOrmModule.forFeature([Cat])],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService], // Xuất CatsService để sử dụng trong các module khác
})
export class CatsModule {}
  • Nhìn vào cấu trúc code, cách xử lý logic luôn được viết trong các đơn vị Service nhỏ nhất, và đóng gói độc lập qua các module, hoặc sử dụng chung logic của nhau ...
  • NestJS cung cấp một cách thuận tiện và mạnh mẽ để phát triển các ứng dụng microservices và là một trong những lựa chọn phổ biến cho việc này trong cộng đồng phát triển ứng dụng Node.js.

Tóm lại ở phần này

Ở phần này mục tiêu của chúng ta là hiểu luồng hoạt động, khai báo của Service, Controller, Module

  • Inject() hiểu hôm na nó gần giống với include Helper trong ROR
  • Controller điều hướng request dựa vào các Decorator @GET, @POST ... (cái này được đề cập khái kỹ trong document) => tuy nhiên nếu anh em nào đã dùng Gem GRAPE trong ROR để làm API thì sẽ thấy tư tưởng khai báo routes ngay trong controller nó khá giống. (tham khảo: https://github.com/ruby-grape/grape)
  • Responsitory gần giống Model trong ROR
  • IOC là quy tắc hiểu ngắn gọn là việc quản lý các thành phần nhỏ nhất từ địa phương đến trung ương thông qua khai báo, inject, ... Ngược với ROR, chúng ta đi từ base sau đó kế thừa sang các phần con. Ví dụ như tạo ApplicationController, và kế thừa nó trong UserController chẳng hạn, là ta sẽ sử dụng được các method của ApplicationController => Nhưng mà UserController thì lại chẳng phải khai báo ở đâu ngoài routes.

middleware, guards

  • Được nhắc đến trong document ở: https://docs.nestjs.com/middlewarehttps://docs.nestjs.com/guards
  • Khái niệm này gần giống khi sử dụng before_action, Authorization, GEM PUNDIT, CANCANCAN (những GEM nổi tiếng cho việc phân quyền của ROR). Bài toán cụ thể là làm một nhiệm vụ A trước khi request được xử lý ở controller chỉ định.
  • Ví dụ check quyền trước khi được vào /profile
// auth.guard.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // Kiểm tra xác thực người dùng ở đây, ví dụ kiểm tra token hoặc session.
    // Nếu xác thực thành công và có quyền truy cập, return true, ngược lại return false.
    return isAuthenticated && hasAccess;
  }
}
  • Tiếp theo, áp dụng Guard này cho endpoint /profile bằng cách sử dụng decorator @UseGuards. Ví dụ:
// profile.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('profile')
export class ProfileController {
  @Get()
  @UseGuards(AuthGuard)
  getProfile() {
    // Mã xử lý cho endpoint /profile
    // Nếu đến đây, có nghĩa rằng Guard đã xác thực và có quyền truy cập.
    return 'Thông tin cá nhân của người dùng.';
  }
}

  • Bây giờ, mọi yêu cầu đến /profile sẽ được chuyển qua Guard trước khi xử lý. Nếu người dùng chưa đăng nhập hoặc không có quyền truy cập, Guard sẽ từ chối yêu cầu và không cho phép truy cập vào endpoint.
  • Nếu Guard chấp nhận yêu cầu, mã xử lý của endpoint sẽ được thực thi.

Kết luận

  • Học ngôn ngữ lập trình mới theo quan điểm của mình thì nên liên hệ với những thứ mình đã biết sẽ giúp dễ hiểu bản chất hơn.
  • Có thể những khái niệm giữa nestjs và ROR đôi khi không giống nhau, tuy nhiên có thể tìm điểm chung hoặc liên hệ với cuộc sống đời thường và bài toán mà chúng giải quyết trong thực tế cũng sẽ là một cách.
  • Viết bài viblo là cách để mình hệ thống lại những thứ mình đã biết, chưa biết, tưởng là mình biết để thỉnh thoảng nhìn lại cũng là một cách ghi nhớ.
  • Hi vọng bài viết sẽ giúp ích cho những anh em đang chập chững học nestjs như mình 🥰

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí