NestJS API 02. Controller & Routing & Module Structure


Ở bài trước mình đã giới thiệu về NestJS Framework và những lý do vì sao chúng ta nên sử dụng cũng như cách cài đặt. Hôm này mình sẽ cùng nhau tìm hiểu một số thành phần quan trọng trong NestJS.

Ngoài cách khởi tạo project bằng NestJS CLI ta hoàn toàn có thể sử dụng git để clone source về

git clone git@github.com:nestjs/typescript-starter.git my-app

Nếu các bạn đã tiếp cận với Java Spring hay Angular thì rất dễ học thằng này, vì syntax nó khá là giống nhau.

Theo dõi source code tại đây, branch dev nha

Link postman để test cho dễ.

Modules

Như mình đã giới thiệu ở bài trước, các thành phần ban đầu được sinh ra, đặc biệt là app.module.ts là root module của ứng dụng. Vậy nên ta hạn chế sử dụng file này, ta dùng nó để nạp những module khác thôi, việc khai báo controller, service, … thì để cho các module con khai báo.

Không giống như những framework khác, Phần lớn đều khuyên các bạn nên dùng lệnh để generate ra các thành phần mà framework đó hỗ trợ, để đỡ mất công khai báo và có thể khai báo không chính xác các thành phần bên trong.

Còn thằng này thì không, mình thấy tạo bằng lệnh hay tạo bằng tay đều như nhau. Đôi khi tạo bằng lệnh nó còn không theo í mình muốn nữa là.

Nhưng riêng module mình khuyên nên dùng lệnh nhang, hihii

nest generate module post

Thông thường tên file, folder mình viết thường, tên class mình viết hoa và cách đặt tên như thế nào cũng được, miễn là nó hoạt động nha.

Mặt định những gì mình code, hay các file sinh ra phụ vụ cho dev nằm trong folder src, cho nên mình sẽ không nhắc đến folder này nữa.

Command chạy xong, sẽ sinh ra folder post, chứa bên trong nó là file post.module.ts

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

@Module({})
export class PostModule {}

PostModule này sẽ tự động khai báo vào trong thằng AppModule. Nếu không thì các bạn tự khai báo vào và sẽ trông như thế này:

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

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

Lúc trước mình có làm việc với thằng Laravel khá là khỏe, mặc định các chức năng mà nó cung cấp đều đã được khai báo sẵn trong thư viện của nó. Nói chung là không phát sinh lỗi.

Các bạn làm việc với nodejs thì đã quá quen với việc tự túc khai báo. Thì thằng nestjs này cũng không khác là mấy, chẳng qua nó cấu trúc hóa cho project của mình, để dễ phát triển về sau.

Trong thằng module thì có 3 thằng quan trọng như bạn đã thấy ở trên:

  • imports: nơi nạp các Module khác vào.

  • controllers: Tất nhiên là để khai báo các controller của các bạn vào. Vì nó chứa Route nên người ta để nó riêng ra cho dễ quản lý á mà, còn quản lý gì thì hỏi thằng nestjs á nha.

  • providers: nơi khai báo các providers hay các @Injectable() vào thì mới có thể sử dụng được. Ví dụ như các file Service hay file Repository. Nói chung là những file gì cần Inject vào trong các class. Nếu không thì nó sẽ báo lỗi. Cái khúc này sau này bạn sẽ thấy có nhiều lỗi, mà nhiều khi mình không biết bị sao luôn á. Mình sẽ đề cập tới ở những bài sau, bài đầu chưa gì mà lòi lỗi thì lại nản mất.

  • exports: Nghĩa là xuất ra, là xuất những cái provider phía trên ra cho những cái module khác dùng. Cái khúc này cũng dễ lỗi lắm nè, mà giờ chưa nói tới.

Còn mốt số cái nữa, nào giờ chưa đụng tới, hẹn bài sau.

Controllers

Controller định nghĩa ra nơi mà xử lý các Request và trả về response đến client.

Khác với Laravel, laravel có nơi khai báo riêng các route, và từ đó gọi tới thằng controller để xử lý. Còn thằng này thì khai báo route trên từng function của thằng controller.

Bắt tay ngay vào tạo controller thôi nào. Như mình có nói ở trên, chúng ta có thể tạo bằng tay hoặc bằng lệnh.

nest generate controller post

Lúc này trong folder post sẽ sinh ra 2 file:

  • post.controller.ts: File này chính là nơi mình viết code xử lý á
  • post.controller.spec.ts: Nơi viết Unit Test và hiện tại chưa cần thiết để đụng tới. Có thể xóa.

PostModule đã tự động import cái PostController này vào trong controlers.

Nếu chưa thì tự mà thêm vào nhá, tương tự AppModule.

Cái bất tiện chỗ này là nó sinh ra file test và nó không nằm trong thư mục mình muốn. Nếu mình tự chuyển vào folder, xong phải cập nhật lại đường dẫn trong module. Tùy sở thích các bạn thôi.

Khai báo một số Route căn bản:

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common';
import { PostService } from './post.service';
import { CreatePostDto, UpdatePostDto } from './dto/post.dto';

@Controller('post')
export class PostController {
  constructor(private readonly postService: PostService) {}

  @Get()
  getAllPosts() {
    return this.postService.getAllPosts();
  }

  @Get(':id')
  getPostById(@Param('id') id: string) {
    return this.postService.getPostById(Number(id));
  }

  @Post()
  async createPost(@Body() post: CreatePostDto) {
    return this.postService.createPost(post);
  }

  @Put(':id')
  async replacePost(@Param('id') id: string, @Body() post: UpdatePostDto) {
    return this.postService.replacePost(Number(id), post);
  }

  @Delete(':id')
  async deletePost(@Param('id') id: string) {
    this.postService.deletePost(Number(id));
    return true;
  }
}

Trong controller, chúng ra sử dụng @Controller() decorator để nestjs hiểu, tham số truyền vào là prefix của controller đó,có thể không cần truyền vào nha.

Trong folder post, tạo file post.interface.ts để định nghĩa cho 1 đối tượng giả vờ mà mình thực hiện trong bài này, thật tế thì ta làm việc với model, nhưng để bài sau nhan.

post.interface.ts

export interface Post {
  id: number;
  content: string;
  title: string;
}

Routing

Phần Routing ta khai báo trực tiếp trên các function, Nest js hỗ trợ hầu hết tất cả các phương thức như @Get(), @Post(), @Put, @Delete, …

Giá trị truyền vào là đường dãn tính tiếp từ controller, có thể không cần truyền vào. Và gọi các phương thức này từ thư viện @nestjs/common.

Return all posts
GET /post

Return a posts with a given id
GET /post/{id}

Create a new post
POST /post

Update a post with a given id
PUT /post/{id}

Remove a post with a given id
DELETE /post/{id}

Mặc định, Nestjs trả về statusCode 200 OK, ngoại từ 201 Created của phương thức POST. Các bạn có thể tùy chỉnh các này, tham khảo thêm @HttpCode() decorator tại đây.

Accessing param, body, query

Khi xử lý 1 api, chúng ta thường dùng những thuộc tính đặc biệt, chúng ta có thể sử dụng route parameters, ta khai báo sử dụng : như đã khai báo ở trên.

Để lấy được giá trị đã khai báo trước đó, ta sử dụng @Param() decorator. Trả về một array các param, hoặc chỉ muốn lấy 1 param duy nhất thì ta truyền param đó vào

 @Param('id') id
 
 // or

 @Param() param

Thằng NestJs này nó phân biệt ra từng loại tham số truyền lên, chớ không có nhận tè le với khai báo là $request như thằng Laravel.

Tương tự với params, thì còn có kiểu @Query()@Body() dùng cho những phương thức khác nhau và cách sử dụng như thằng @Param() vậy.

  • @Query(): thường dùng cho phương thức @Get()@Delete()
  • @Body(): dùng cho các phương thức còn lại như @Post(), @Put(), …

Thông thường thì @Query@Body() ít khi dùng chung, dùng cũng được nhưng mất công phải khai báo nhiều. Phía FE gọi api cũng mất công, nói chung là không nên.

Theo dõi các tham số tuyền vào dưới đây:

async replacePost(@Body() post: UpdatePostDto, @Param('id') id: string) {
  return this.postsService.replacePost(Number(id), post);
}

Các bạn thấy thằng UpdatePostDto, nó là 1 class dùng để định nghĩa thằng @Body() hay thằng @Query(). Trong này mình có thể validate những tham số truyền lên. Mình sẽ giới thiệu trong những bài sau.

Trong folder post, ta tạo folder dto, bên trong đó tạo file post.dto.ts

class CreatePostDto {
  content: string;
  title: string;
}

class UpdatePostDto {
  id: number;
  content: string;
  title: string;
}

Khai báo các function controler, các tham số có thể hoán đổi vị trí cho nhau mà không làm mất đi giá trị thật của nó:

async replacePost(@Param('id') id: string, @Body() post: UpdatePostDto) {
  return this.postsService.replacePost(Number(id), post);
}

Service

Service là những Injectable() mà phía trên modules mình có nói. Nhiệm vụ của nó là tách các business login từ controller ra , làm nó trông clear hơn và dễ dàng kiểm tra.

Ta có thể dùng lệnh hoặc tạo bằng tay tùy thích, và cũng như thằng controller, nó cũng sinh ra 2 file, và giả sử như 1 module có nhiều service và nhiều controller, có phải là nó trông rất rối không, thay vì đó ta tạo cho mỗi chức nămg 1 folder riêng, và chứa các file chức năng bên trong nó, thì sẽ gọn gàng hơn nhiều.

post.service.ts

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

@Injectable()
export class PostService {
  private lastPostId = 0;
  private posts: Post[] = [];

  getAllPosts() {
    return this.posts;
  }

  getPostById(id: number) {
    const post = this.posts.find((post) => post.id === id);
    if (post) {
      return post;
    }
    throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
  }

  replacePost(id: number, post: UpdatePostDto) {
    const postIndex = this.posts.findIndex((post) => post.id === id);
    if (postIndex > -1) {
      this.posts[postIndex] = <Post>post;
      return post;
    }
    throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
  }

  createPost(post: CreatePostDto) {
    const newPost = {
      id: ++this.lastPostId,
      ...post,
    };
    this.posts.push(<Post>newPost);
    return newPost;
  }

  deletePost(id: number) {
    const postIndex = this.posts.findIndex((post) => post.id === id);
    if (postIndex > -1) {
      this.posts.splice(postIndex, 1);
    } else {
      throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
    }
  }
}

Và khai báo PostService vào trong PostModule

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

@Module({
  controllers: [PostController],
  providers: [PostService],
})
export class PostModule {}

Code này toàn là basic, có thằng HttpExeption hơi khác 1 xíu, nó trả về data theo format:

{
  "statusCode": 404,
  "message": "Post not found",
}

Sau khi hoàn tất, project ta có dạng

├── src
│   ├── app.module.ts
│   ├── main.ts
│   └── post
│       ├── dto
│       │   ├── post.dto.ts
│       ├── post.interface.ts
│       ├── post.controller.ts
│       ├── post.module.ts
│       └── post.service.ts

Kết luận

Trong bài viết này, chúng ta vừa tìm hiểu về các thành phần quan trọng nhất của NestJS, về cách khởi tạo cũng như khai báo để nó hoặc động,

Serie lần này làm việc giữa NestJS, Mongodb và Redis, các bạn chuẩn bị tinh thần nhá.

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!


Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Nguyễn Quang Đạt !
Comments
  TOC