2024.12.14

Phần 1: Hướng dẫn xây dựng REST API với NestJS và Prisma một cách CLEAR nhất.

MEVN REST API with NestJS and Prisma

NestJS là một trong những framework nổi bật của Node.js và gần đây đã nhận được rất nhiều sự yêu thích và quan tâm từ các nhà phát triển. Trong quá trình tìm kiếm tài liệu mình có đọc được một chuỗi bài viết khá hay về cách xây dựng một ứng dụng REST API […]

NestJS là một trong những framework nổi bật của Node.js và gần đây đã nhận được rất nhiều sự yêu thích và quan tâm từ các nhà phát triển. Trong quá trình tìm kiếm tài liệu mình có đọc được một chuỗi bài viết khá hay về cách xây dựng một ứng dụng REST API backend với NestJS, Prisma, PostgreSQL và Swagger. Mình xin phép được chia sẻ lại (Việt hóa) theo những gì mình hiểu trong quá trình thực hành để mọi người dễ theo dõi hơn.

Nội dung chính

  • Giới thiệu
  • Yêu cầu
    • Kiến thức giả định
    • Môi trường phát triển
  • Tạo dự án NestJS
  • Tạo một PostgreSQL Instance
  • Thiết lập Prisma
    • Khởi tạo Prisma
    • Cài đặt biến môi trường
    • Hiểu về Prisma schema
    • Mô hình hóa dữ liệu
    • Migrate cơ sở dữ liệu
    • Seed cơ sở dữ liệu
    • Tạo Prisma service
  • Thiết lập Swagger
  • Thực hiện các thao tác CRUD cho Product model
    • Tạo REST resources
    • Thêm PrismaClient vào Products module
    • Định nghĩa endpoint GET /products
    • Định nghĩa endpoint GET /products/drafts
    • Định nghĩa endpoint GET /products/:id
    • Định nghĩa endpoint POST /products
    • Định nghĩa endpoint PATCH /products/:id
    • Định nghĩa endpoint DELETE /products/:id
    • Nhóm các endpoint trong Swagger
  • Cập nhật response type trong Swagger
  • Tóm tắt và nhận xét

Giới thiệu

Trong hướng dẫn này, bạn sẽ học cách xây dựng backend REST API cho một ứng dụng quản lý sản phẩm (Products-Management). Bạn sẽ bắt đầu bằng cách tạo một dự án NestJS mới, sau đó khởi tạo server PostgreSQL và kết nối với nó thông qua Prisma. Cuối cùng, bạn sẽ xây dựng REST API và tạo tài liệu cho nó bằng Swagger.

Các công nghệ bạn sẽ sử dụng

Trong bài viết này, chúng ta sẽ sử dụng các công nghệ sau:

  • NestJS: Framework mạnh mẽ dựa trên Node.js, giúp phát triển backend nhanh chóng và linh hoạt.
  • Prisma: ORM (Object-Relational Mapping)  để làm việc với cơ sở dữ liệu một cách dễ dàng.
  • PostgreSQL: Hệ quản trị cơ sở dữ liệu quan hệ phổ biến và mạnh mẽ.
  • Swagger: Công cụ tạo API documents tự động và hỗ trợ kiểm thử trực quan.
  • Typescript: Ngôn ngữ lập trình ta sẽ sử dụng ở bài blog này.

Yêu cầu

Kiến thức giả định:

Đây là một hướng dẫn phù hợp với người mới bắt đầu. Tuy nhiên, hướng dẫn này giả định rằng bạn có:

  • Kiến thức cơ bản về JavaScript hoặc TypeScript (ưu tiên TypeScript)
  • Kiến thức cơ bản về NestJS

Lưu ý: Nếu bạn chưa quen với NestJS, bạn có thể nhanh chóng tìm hiểu những kiến ​​thức cơ bản bằng cách đọc qua phần tổng quan trong tài liệu NestJS.

Môi trường phát triển

Để làm theo hướng dẫn này, bạn cần:

  • Cài đặt Node.js.
  • Cài đặt Docker hoặc PostgreSQL.
  • Cài đặt Prisma VSCode Extension. (không bắt buộc)
  • Có quyền truy cập vào một Unix shell (như terminal/shell trong Linux và macOS) để chạy các lệnh trong loạt bài này. (không bắt buộc)

Lưu ý 1: Prisma VSCode Extension không bắt buộc cài đặt, nhưng nó cung cấp IntelliSense hỗ trợ màu sắc trực quan, dễ đọc.

Lưu ý 2: Nếu bạn không có Unix shell (ví dụ: bạn đang sử dụng máy Windows), bạn vẫn có thể làm theo hướng dẫn, nhưng các lệnh shell có thể cần được chỉnh sửa để phù hợp với máy của bạn.

Khởi tạo dự án NestJS

Điều đầu tiên bạn cần làm là cài đặt NestJS CLI. NestJS CLI rất tiện dụng khi làm việc với dự án NestJS. Nó đi kèm với các tiện ích tích hợp giúp bạn khởi tạo, phát triển và duy trì ứng dụng NestJS của mình.

Bạn có thể sử dụng NestJS CLI để tạo một dự án trống. Để bắt đầu, hãy chạy lệnh sau tại thư mục mà bạn muốn dự án sẽ được lưu trữ:

npx @nestjs/cli new products-management

CLI sẽ yêu cầu bạn chọn trình quản lý package cho dự án của bạn — hãy chọn npm. Sau đó, dự án NestJS sẽ được tự động tạo mới trong thư mục hiện tại.

Hãy mở dự án bằng IDE bất kỳ (khuyến nghị sử dụng VSCode). Bạn sẽ thấy các tệp sau:

products-management
  ├── node_modules
  ├── src
  │   ├── app.controller.spec.ts
  │   ├── app.controller.ts
  │   ├── app.module.ts
  │   ├── app.service.ts
  │   └── main.ts
  ├── test
  │   ├── app.e2e-spec.ts
  │   └── jest-e2e.json
  ├── README.md
  ├── nest-cli.json
  ├── package-lock.json
  ├── package.json
  ├── tsconfig.build.json
  └── tsconfig.json

Bạn sẽ thao tác chủ yếu ở thư mục src. NestJS CLI đã tạo sẵn một vài tập tin cho bạn. Một số tập tin đáng chú ý là:

  • src/app.module.ts: Root Module của ứng dụng.
  • src/app.controller.ts: Một controller cơ bản với một route: /. Route này sẽ trả về một thông báo đơn giản ‘Hello World!’.
  • src/main.ts: Entry point của ứng dụng. Nó sẽ khởi động ứng dụng NestJS.

Bạn có thể start dự án của mình bằng cách sử dụng lệnh sau:

npm run start:dev

Lệnh này sẽ theo dõi các tập tin của bạn, tự động biên dịch và tải lại server mỗi khi bạn thực hiện thay đổi. Để đảm bảo rằng server đang chạy, hãy truy cập vào URL http://localhost:3000/. Bạn sẽ thấy một trang mặc định với thông báo 'Hello World!'.

Lưu ý: Bạn nên giữ server chạy ở chế độ nền trong suốt quá trình thực hiện theo hướng dẫn này.

Khởi tạo PostgreSQL instance

Chúng ta sẽ sử dụng PostgreSQL làm cơ sở dữ liệu cho ứng dụng NestJS này. Hướng dẫn này sẽ chỉ cho bạn cách cài đặt và chạy PostgreSQL trên máy thông qua Docker container.

Lưu ý: Nếu bạn không muốn sử dụng Docker, bạn có thể cài đặt PostgreSQL trực tiếp trên máy hoặc sử dụng dịch vụ cơ sở dữ liệu PostgreSQL được lưu trữ trên Heroku.

Đầu tiên, hãy tạo một tệp docker-compose.yml trong thư mục chính của dự án:

touch docker-compose.yml

Tệp docker-compose.yml là một tệp cấu hình, chứa các thông số để chạy một Docker container với PostgreSQL được cài đặt bên trong. Tạo cấu hình sau bên trong tệp:

# docker-compose.yml

version: '3.8'
services:

  postgres:
    image: postgres:13.5
    restart: always
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - '5432:5432'

volumes:
  postgres:
  • image: Xác định Docker image sẽ sử dụng. Ở đây, bạn đang sử dụng postgres image version 13.5.
  • environment: Chỉ định các biến môi trường được truyền vào container trong quá trình khởi tạo. Bạn có thể định nghĩa các tùy chọn cấu hình và thông tin bảo mật như tên người dùng và mật khẩu mà container sẽ sử dụng tại đây.
  • volumes: Được sử dụng để lưu trữ dữ liệu trong hệ thống.
  • ports: Ánh xạ cổng từ máy chủ sang container. Định dạng theo quy ước host_port:container_port. Trong trường hợp này, bạn đang ánh xạ cổng 5432 của máy chủ sang cổng 5432 củapostgres container. 5432 thường là cổng được sử dụng bởi PostgreSQL.

Hãy đảm bảo rằng không có gì đang chạy trên cổng 5432 của máy bạn. Để khởi động postgres container, mở một cửa sổ terminal mới và chạy lệnh sau trong thư mục chính của dự án:

docker-compose up

Nếu mọi thứ hoạt động chính xác, cửa sổ terminal mới sẽ hiển thị các log thông báo rằng hệ thống cơ sở dữ liệu đã sẵn sàng để chấp nhận kết nối.

postgres-1  | 2024-12-04 03:29:37.464 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
postgres-1  | 2024-12-04 03:29:37.464 UTC [1] LOG:  listening on IPv6 address "::", port 5432
postgres-1  | 2024-12-04 03:29:37.467 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres-1  | 2024-12-04 03:29:37.474 UTC [62] LOG:  database system was shut down at 2024-12-04 03:29:37 UTC
postgres-1  | 2024-12-04 03:29:37.478 UTC [1] LOG:  database system is ready to accept connections

Chúc mừng bạn đã tạo thành công, bây giờ bạn hãy chuẩn bị một tâm lý thật thoải mái để có thể khám phá những điều rất thú vị tiếp theo.

Lưu ý: Nếu bạn đóng cửa sổ terminal, nó cũng sẽ dừng container. Bạn có thể tránh điều này bằng cách thêm tùy chọn -d vào cuối lệnh, như sau: docker-compose up -d. Điều này sẽ chạy container ở chế độ nền vô thời hạn.

Thiết lập Prisma

Bây giờ cơ sở dữ liệu đã sẵn sàng, đã đến lúc thiết lập Prisma!

Khởi tạo Prisma

Để bắt đầu, trước tiên hãy cài đặt Prisma CLI để thực hiện một số tác vụ. Prisma CLI sẽ cho phép bạn chạy nhiều lệnh khác nhau và tương tác với dự án của bạn.
npm install -D prisma
Bạn có thể khởi tạo Prisma bên trong dự án của mình bằng cách chạy lệnh sau:
npx prisma init
Lệnh này sẽ tạo một thư mục prisma mới với tệp schema.prisma. Đây là tệp cấu hình chính chứa schema của cơ sở dữ liệu. Lệnh này cũng tạo một tệp .env bên trong dự án của bạn.

Cài đặt biến môi trường

Bên trong tệp .env, bạn sẽ thấy một biến môi trường DATABASE_URL với một connection string (chuỗi kết nối) theo một template. Hãy thay thế chuỗi kết nối này bằng chuỗi kết nối vói PostgreSQL của bạn.

//.env
DATABASE_URL="postgres://myuser:mypassword@localhost:5432/postgres"

Lưu ý: Nếu bạn không sử dụng Docker (như đã chỉ ra trong phần trước) để tạo cơ sở dữ liệu PostgreSQL, chuỗi kết nối của bạn sẽ khác với chuỗi được hiển thị ở trên. Định dạng chuỗi kết nối cho PostgreSQL có sẵn trong tài liệu Prisma.

Giải thích một chút về Prisma schema

Nếu bạn mở prisma/schema.prisma, bạn sẽ thấy  schema mặc định sau:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Tệp này được viết bằng Prisma Schema Language, là một ngôn ngữ mà Prisma sử dụng để định nghĩa schema cho cơ sở dữ liệu của bạn. Tệp schema.prisma có ba thành phần chính:

  • Data source: Chỉ định kết nối đến cơ sở dữ liệu của bạn. Cấu hình ở trên có nghĩa provider là PostgreSQL và chuỗi kết nối cơ sở dữ liệu được lưu trong biến môi trường DATABASE_URL.
  • Generator: Chỉ định rằng bạn muốn tạo Prisma Client, một trình xây dựng truy vấn type-safe cho cơ sở dữ liệu của bạn. Nó được sử dụng để gửi các truy vấn đến cơ sở dữ liệu.
  • Data model: Định nghĩa các models để tạo database của bạn. Mỗi model sẽ được ánh xạ thành một bảng trong cơ sở dữ liệu. Hiện tại chưa có model nào trong schema của bạn, phần này sẽ được tìm hiểu trong phần tiếp theo.

Lưu ý: Để biết thêm thông tin về schema của Prisma, hãy xem Prisma Docs.

Mô hình hóa dữ liệu

Bây giờ là lúc định nghĩa các models dữ liệu cho ứng dụng của bạn. Trong hướng dẫn này, bạn chỉ cần một model Product để đại diện cho mỗi sản phẩm của bạn.

Bên trong tệp prisma/schema.prisma, hãy thêm một model mới vào schema của bạn có tên là Product:

model Product {
  id          Int      @id @default(autoincrement())
  name        String   @unique
  description String?
  published   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

Ở đây, bạn đã tạo một model Product với một số trường. Mỗi trường có một tên (id, name, v.v.), một kiểu dữ liệu (Int, String, v.v.), và các thuộc tính tùy chọn khác (như @id, @unique, v.v.). Các trường optional có thể  định nghĩa bằng cách thêm dấu ? sau kiểu dữ liệu của trường đó.

  • Trường id có một thuộc tính đặc biệt là @id. Thuộc tính này chỉ ra rằng trường này là khóa chính của model. Thuộc tính @default(autoincrement()) chỉ ra rằng trường này sẽ được tự động tăng và gán cho mỗi bản ghi mới được tạo.
  • Trường published là một cờ để chỉ ra sản phẩm đã được công khai hay ẩn đi. Thuộc tính @default(false) chỉ ra rằng trường này sẽ được đặt giá trị false theo mặc định.
  • Hai trường DateTimecreatedAtupdatedAt sẽ theo dõi thời điểm tạo và thời điểm cuối cùng sản phẩm được cập nhật. Thuộc tính @updatedAt sẽ tự động cập nhật trường này với thời gian hiện tại mỗi khi sản phẩm được sửa đổi.

Migrate cơ sở dữ liệu

Với Prisma schema vừa được định nghĩa, bạn sẽ phải thực hiện migrations để tạo các bảng thực tế trong cơ sở dữ liệu. Để tạo và thực thi migration lần đầu tiên, hãy chạy lệnh sau trong terminal:

npx prisma migrate dev --name "init"

Lệnh này sẽ thực hiện ba việc:

  1. Lưu migration: Prisma Migrate sẽ ghi lại một bản sao đối với schema của bạn và xác định các lệnh SQL cần thiết để thực hiện migration. Prisma sẽ lưu tệp migration chứa các lệnh SQL vào thư mục prisma/migrations mới được tạo.
  2. Thực thi migration: Prisma Migrate sẽ thực thi các lệnh SQL trong tệp migration để tạo các bảng trong cơ sở dữ liệu của bạn.
  3. Tạo Prisma Client: Prisma sẽ tạo Prisma Client dựa trên schema mới nhất của bạn. Vì bạn chưa cài đặt thư viện Client, CLI cũng sẽ tự động cài đặt nó cho bạn. Bạn sẽ thấy gói @prisma/client trong phần dependencies của tệp package.json. Prisma Client là một TypeScript query builder  được tạo tự động từ schema của Prisma. Nó được điều chỉnh phù hợp với schema của bạn và sẽ được sử dụng để gửi truy vấn đến cơ sở dữ liệu.

Lưu ý: Bạn có thể tìm hiểu thêm về Prisma Migrate trong tài liệu Prisma.

Nếu chạy lệnh thành công, bạn sẽ thấy thông báo như sau:

Kết quả mirate database

Kiểm tra tệp migration được tạo để hiểu rõ hơn về những gì Prisma Migrate đã thực hiện trong dự án của bạn:

Nội dung file migrations

Lưu ý: Tên tệp schema của bạn sẽ hơi khác một chút.

Đây là SQL cần thiết để tạo table Product bên trong cơ sở dữ liệu PostgreSQL của bạn. Nó được Prisma tự động tạo và thực thi dựa trên Prisma schema.

Seed cơ sở dữ liệu

Hiện tại, cơ sở dữ liệu đang trống. Vì vậy, bạn sẽ tạo một seed script để điền một số dữ liệu mẫu vào cơ sở dữ liệu.

Đầu tiên, hãy tạo một tệp seed có tên là prisma/seed.ts. Tệp này sẽ chứa dữ liệu mẫu và các truy vấn cần thiết để điền vào cơ sở dữ liệu của bạn.

touch prisma/seed.ts

Sau đó, bên trong tệp seed.ts, thêm đoạn mã sau:

// prisma/seed.ts

import { PrismaClient } from '@prisma/client';

// initialize Prisma Client
const prisma = new PrismaClient();

async function main() {
  const product1 = await prisma.product.upsert({
    where: { name: 'MEVN Prodcut 1' },
    update: {},
    create: {
      name: 'MEVN Product1',
      description:
        'This is MEVN Product1. This is a description of MEVN Product1.',
      published: false,
    },
  });
  const product2 = await prisma.product.upsert({
    where: { name: 'MEVN Product2' },
    update: {},
    create: {
      name: 'MEVN Product2',
      description:
        'This is MEVN Product2. This is a description of MEVN Product2.',
      published: false,
    },
  });

  console.log({ product1, product2 });
}

// execute the main function
main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    // close Prisma Client at the end
    await prisma.$disconnect();
  });

Bên trong script này, bạn sẽ khởi tạo Prisma Client trước. Sau đó, bạn tạo hai sản phẩm cách sử dụng hàm prisma.upsert(). Hàm upsert chỉ tạo sản phẩm mới nếu không có sản phẩm nào phù hợp với điều kiện where. Bạn sử dụng truy vấn upsert thay vì truy vấn createupsert giúp tránh lỗi liên quan đến việc vô tình cố gắng chèn cùng một bản ghi hai lần.

Bạn cần chỉ định cho Prisma biết script nào sẽ được thực thi khi chạy lệnh seeding. Bạn có thể làm điều này bằng cách thêm key prisma.seed vào cuối tệp package.json:

// package.json

// ...
  "scripts": {
    // ...
  },
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
  },
  "jest": {
    // ...
  },
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }

Lệnh seed sẽ thực thi script prisma/seed.ts mà bạn đã định nghĩa trước đó. Lệnh này sẽ hoạt động tự động vì ts-node đã được cài đặt dưới dạng dev dependency trong tệp package.json của bạn.

Thực hiện seeding bằng lệnh sau:

npx prisma db seed

Lệnh này sẽ chạy script seed để điền dữ liệu mẫu vào cơ sở dữ liệu của bạn. Sau khi thực thi xong, bạn sẽ thấy thông báo như sau:

Seed data vào PostreSQL với Prisma

Lưu ý: Bạn có thể đọc thêm về seeding ở Prisma Docs

Tạo Prisma service

Trong ứng dụng NestJS, bạn nên tách Prisma Client API ra khỏi ứng dụng. Để làm điều này, bạn sẽ tạo một service mới để chứa Prisma Client. Service này, gọi là PrismaService, sẽ chịu trách nhiệm khởi tạo một instance của PrismaClient và kết nối với cơ sở dữ liệu của bạn.

Nest CLI cung cấp cách dễ dàng để tạo moduleservice trực tiếp từ dòng lệnh (CLI). Chạy lệnh sau trong terminal của bạn:

npx nest generate module prisma

npx nest generate service prisma

Lưu ý 1: Nếu cần, hãy tham khảo tài liệu của NestJS để có cái nhìn tổng quan về servicesmodules.

Lưu ý 2: Trong một số trường hợp, khi chạy lệnh nest generate trong khi server đang chạy, NestJS có thể throw exception với thông báo: Error: Cannot find module './app.controller'. Nếu gặp phải lỗi này, hãy chạy lệnh sau từ terminal. Sau đó, khởi động lại server của bạn.

rm -rf dist

Lệnh này sẽ tạo một thư mục con mới ./src/prisma với hai tệp: prisma.module.tsprisma.service.ts. Tệp service (prisma.service.ts) sẽ chứa đoạn mã sau:

// src/prisma/prisma.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient {}

Prisma Module sẽ chịu trách nhiệm tạo một instance singleton của PrismaService và cho phép chia sẻ service này trong toàn bộ ứng dụng của bạn. Để làm điều này, bạn cần thêm PrismaService vào mảng exports trong tệp prisma.module.ts:

// src/prisma/prisma.module.ts

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Bây giờ, bất kỳ module nào import PrismaModule sẽ có quyền truy cập vào PrismaService và có thể tiêm (inject) nó vào các thành phần hoặc service của riêng mình. Đây là một design parttern thông dụng trong các ứng dụng NestJS.

Với bước này, bạn đã hoàn thành việc thiết lập Prisma! Giờ đây, bạn có thể bắt đầu làm việc để xây dựng REST API.

Thiết lập Swagger

Swagger là một công cụ để tài liệu hóa API của bạn bằng cách sử dụng tiêu chuẩn OpenAPI specification. NestJS có một module dành riêng cho Swagger, và bạn sẽ sử dụng nó để tài liệu hóa API của mình.

Swagger giúp bạn tạo tài liệu dễ đọc và tương tác, cho phép bạn và các nhà phát triển khác có thể kiểm tra và sử dụng API một cách thuận tiện mà không cần phải đọc qua toàn bộ mã nguồn hoặc tài liệu dài dòng.

Bắt đầu bằng cách cài đặt các thư viện cần thiết:

npm install --save @nestjs/swagger swagger-ui-express

Mở tệp main.ts và khởi tạo Swagger bằng cách sử dụng lớp SwaggerModule:

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('MEVN Products Management')
    .setDescription('MEVN Products Managemen API description')
    .setVersion('0.1')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

Trong khi ứng dụng đang chạy, hãy mở trình duyệt của bạn và truy cập vào địa chỉ http://localhost:3000/api. Bạn sẽ thấy giao diện Swagger UI.

Sử dụng Swagger để Documenets REST API

Triển khai REST API CRUD cho model Product:

Trong phần này, bạn sẽ triển khai các thao tác Create (tạo), Read (đọc), Update (cập nhật), và Delete (xóa) cho  model Product và bất kỳ logic nghiệp vụ liên quan nào.

Tạo REST resources

Trước khi bạn có thể triển khai REST API, bạn cần tạo REST resouces cho mô hình Product. Điều này có thể được thực hiện nhanh chóng bằng Nest CLI. Hãy chạy lệnh sau trong terminal của bạn:

Sau khi trả lời các câu hỏi từ CLI:

  1. What name would you like to use for this resource (plural, e.g., “users”)?  Bạn nhập products.
  2. What transport layer do you use? Chọn REST API.
  3. Would you like to generate CRUD entry points? Chọn Yes.

Bạn sẽ thấy một thư mục mới có tên src/products với tất cả mã mẫu (boilerplate) cho các endpoint REST của bạn.

  • Bên trong tệp src/products/products.controller.ts, bạn sẽ thấy định nghĩa của các route khác nhau (còn gọi là route handlers).
  • Business logic cho việc xử lý từng yêu cầu được gói gọn trong tệp src/products/products.service.ts. Hiện tại, tệp này chứa các code demo.

Nếu bạn mở lại trang Swagger API, bạn sẽ thấy giao diện hiển thị các endpoint CRUD cho products, cho phép bạn kiểm tra và tương tác với API của mình trực tiếp thông qua Swagger.

Swagger API documents

SwaggerModule tìm kiếm tất cả các decorator @Body(), @Query(), và @Param() trên các route handlers để tự động tạo trang API này.

Thêm PrismaClient vào Products module

Để truy cập PrismaClient bên trong module Products, bạn cần thêm PrismaModule vào phần import của ProductsModule. Hãy thêm các dòng import sau vào tệp products.module.ts:

// src/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
import { PrismaModule } from 'src/prisma/prisma.module';

@Module({
  controllers: [ProductsController],
  providers: [ProductsService],
  imports: [PrismaModule],
})
export class ProductsModule {}

Bây giờ bạn có thể inject PrismaService vào ProductsService và sử dụng nó để truy cập cơ sở dữ liệu. Để làm điều này, hãy thêm một constructor vào tệp products.service.ts như sau:

// src/products/products.service.ts

import { Injectable } from '@nestjs/common';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class ProductsService {
  constructor(private prisma: PrismaService) {}

  // CRUD operations
}

Định nghĩa GET /products endpoint

Controller cho endpoint này có tên là findAll. Endpoint này sẽ trả về tất cả products đã được tạo với cờ là published: true trong cơ sở dữ liệu. Controller findAll trông như sau:

// src/products/products.controller.ts

@Get()
findAll() {
   return this.productsService.findAll();
}

Bạn cần cập nhật ProductsService.findAll() để trả về một mảng tất cả các product đã được published trong cơ sở dữ liệu:

// src/products/products.service.ts

@Injectable()
export class ProductsService {
  constructor(private prisma: PrismaService) {}
  
  create(createProductDto: CreateProductDto) {
    return 'This action adds a new product';
  }
  
  findAll() {
    return this.prisma.product.findMany({ where: { published: true } });
  }
}

Truy vấn findMany sẽ trả về tất cả các bản ghi product khớp với điều kiện where.

Bạn có thể kiểm tra endpoint này bằng cách truy cập vào http://localhost:3000/api và nhấp vào dropdown menu GET/products. Nhấn Try it out rồi Execute để xem kết quả.

Get products với Swagger

Lưu ý: Bạn cũng có thể chạy tất cả các request trực tiếp trong trình duyệt hoặc thông qua một REST client (như Postman). Swagger cũng tự động tạo các lệnh curl cho mỗi yêu cầu, trong trường hợp bạn muốn chạy các yêu cầu HTTP trong terminal.

Định nghĩa GET /products/drafts endpoint

Bạn sẽ định nghĩa một route mới để lấy tất cả products chưa được xuất bản (unpublished). NestJS không tự động tạo  route handler cho endpoint này, vì vậy bạn cần viết nó thủ công.

// src/products/products.controller.ts

@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Post()
  create(@Body() createProductDto: CreateProductDto) {
    return this.productsService.create(createProductDto);
  }

  @Get('drafts')
  findAllDrafts() {
    return this.productsService.findDrafts();
  }
  //...
}

Trình soạn thảo của bạn sẽ hiển thị lỗi rằng không có hàm nào tên là productsService.findDrafts(). Để khắc phục lỗi này, bạn cần triển khai phương thức findDrafts trong ProductsService:

// src/products/products.service.ts

@Injectable()
export class ProductsService {
  constructor(private prisma: PrismaService) {}
  
  create(createProductDto: CreateProductDto) {
    return 'This action adds a new product';
  }

  findDrafts() {
    return this.prisma.product.findMany({ where: { published: false } });
  }

  findAll() {
    return this.prisma.product.findMany({ where: { published: true } });
  }
}

Bây giờ bạn có thể truy cập endpoint GET /products/drafts bằng cách load lại trang Swagger API page.

Lưu ý: bạn nên kiểm tra từng endpoint thông qua trang API Swagger sau khi hoàn tất quá trình implement endpoint đó.

Định nghĩa GET /products/:id endpoint

Handler cho route của controller này có tên là findOne. Nó trông như sau:

// src/products/products.controller.ts

@Get(':id')
 findOne(@Param('id') id: string) {
   return this.productsService.findOne(+id);
 }

Route này chấp nhận tham số id động, và tham số này được truyền vào handler findOne của controller. Vì Product model có trường id là kiểu số nguyên, nên tham số id cần được chuyển đổi thành kiểu số bằng cách sử dụng toán tử +.

Bây giờ, hãy cập nhật phương thức findOne trong ProductsService để trả về product với id đã cung cấp:

// src/products/products.service.ts

@Injectable()
export class ProductsService {
  constructor(private prisma: PrismaService) {}
  
  create(createProductDto: CreateProductDto) {
    return 'This action adds a new product';
  }

  findAll() {
    return this.prisma.product.findMany({ where: { published: true } });
  }

  findOne(id: number) {
    return this.prisma.product.findUnique({ where: { id } });
  }
}

Một lần nữa, hãy kiểm tra endpoint này bằng cách truy cập vào http://localhost:3000/api. Nhấp vào menu dropdown GET /products/{id}. Nhấn Try it out, thêm một giá trị hợp lệ vào tham số id, sau đó nhấn Execute để xem kết quả.

Enpoint GET /products/:id

Định nghĩa POST /products endpoint

Đây là endpoint để tạo mới các product. Handler của controller cho endpoint này có tên là create. Nó trông như sau:

// src/products/products.controller.ts
@Post()
create(@Body() createProductDto: CreateProductDto) {
  return this.productsService.create(createProductDto);
}

Lưu ý rằng phương thức này mong đợi các tham số có kiểu CreateProductDto trong phần request body. DTO (Data Transfer Object) là một đối tượng định nghĩa cách dữ liệu sẽ được gửi qua network. Hiện tại, CreateProductDto là một lớp trống. Bạn sẽ thêm các thuộc tính vào đó để xác định cấu trúc của phần request body.

//src/products/dto/create-product.dto.ts 

import { ApiProperty } from '@nestjs/swagger';

export class CreateProductDto {
    @ApiProperty()
    name: string;

    @ApiProperty({ required: false })
    description?: string;

    @ApiProperty({ required: false, default: false })
    published?: boolean = false;
}

Để các thuộc tính của lớp `CreateProductDto` được hiển thị SwaggerModule cần thêm vào các decorator @ApiProperty . Xem thêm về decorator trong tài liệu của NestJS.

CreateProductDto bây giờ sẽ được hiển thị trên trang Swagger API dưới phần Schemas. Cấu trúc của UpdateProductDto được  kế thừa từ CreateProductDto. Vì vậy, UpdateProductDto cũng được định nghĩa bên trong Swagger. Điều này giúp Swagger hiểu và mô tả được các yêu cầu của API, từ đó làm cho tài liệu của bạn dễ đọc và dễ sử dụng hơn khi thử nghiệm và tương tác với các endpoint.

DTO create, update

Bây giờ hãy cập nhật phương thức create trong ProductsService để tạo một product mới trong cơ sở dữ liệu.

// src/products/products.service.ts

@Injectable()
export class ProductsService {
  constructor(private prisma: PrismaService) {}
  
  create(createProductDto: CreateProductDto) {
    return this.prisma.product.create({ data: CreateProductDto });
  }

  //...
}

Định ngĩa PATCH /products/:id endpoint

Đây là endpoint để cập nhật các product đã tồn tại. Handler của route cho endpoint này có tên là update. Nó trông như sau:

// src/products/products.controller.ts

@Patch(':id')
update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {
  return this.productsService.update(+id, updateProductDto);
}

UpdateProductDto được kế thừa từ PartialType của CreateProductDto, vì vậy nó có thể bao gồm tất cả các thuộc tính của CreateProductDto, nhưng tất cả các thuộc tính này đều là optional.

// src/products/dto/update-product.dto.ts

import { PartialType } from '@nestjs/swagger'; 
import { CreateProductDto } from './create-product.dto'; 
export class UpdateProductDto extends PartialType(CreateProductDto) {}

Tương tự, bạn cần cập nhật phương thức tương ứng trong ProductsService để xử lý thao tác này.

@Injectable()
export class ProductsService {
  constructor(private prisma: PrismaService) {}

  //...

  update(id: number, updateProductDto: UpdateProductDto) {
    return this.prisma.product.update({
      where: { id },
      data: updateProductDto,
    });
  }

  //...

}

Phương thức product.update sẽ cố gắng tìm một bản ghi Product với id được cung cấp và cập nhật nó với dữ liệu từ updateProductDto.

Nếu không tìm thấy bản ghi Product nào trong cơ sở dữ liệu với id đó, Prisma sẽ trả về một lỗi. Trong những trường hợp như vậy, API không cung cấp thông báo lỗi thân thiện với người dùng.Mình sẽ chia sẻ cách xử lý lỗi với NestJS ở một bài viết tiếp theo. Tạm thời, bạn có thể để lỗi hiển thị như mặc định hoặc xử lý đơn giản bằng cách kiểm tra kết quả trước khi trả về.

Định ngĩa DELETE /products/:id endpoint

Đây là endpoint để xóa các product đã tồn tại. Handler cho route của endpoint này có tên là remove. Nó trông như sau:

// src/products/products.controller.ts

@Delete(':id')
remove(@Param('id') id: string) {
  return this.productsService.remove(+id);
}

Giống như trước, hãy vào ProductsService và cập nhật phương thức tương ứng:

// src/products/products.service.ts

@Injectable()
export class ProductsService {
  constructor(private prisma: PrismaService) {}
  
  create(createProductDto: CreateProductDto) {
    return this.prisma.product.create({ data: CreateProductDto });
  }

  //...
  remove(id: number) {
     return this.prisma.product.delete({ where: { id } });
  }
}


Vậy là chúng ta cũng đã implement xong endpoint cuối cùng cho model product. Xin chúc mừng, API của bạn gần như đã hoàn thiện! 🎉

Nhóm các endpoints với nhau trên Swagger

Để nhóm tất cả các endpoint products lại với nhau trong Swagger, bạn cần thêm decorator @ApiTags vào class ProductsController.  Theo dõi code bên dưới:

// src/products/products.controller.ts 

import { ApiTags } from '@nestjs/swagger';

@Controller('products') 
@ApiTags('products') 

export class ProductsController {
   // ... 
}

Các endpoint của product model sẽ được nhóm lại với nhau trên API page.

Group API in swagger

Cập nhật response type cho Swagger

Nếu bạn để ý ở phần Responses của mỗi endpoint, tab Description hiện đang rỗng. Để hiển thị thông tin chi tiết hơn trong tab Description của mỗi endpoint trên Swagger, bạn cần định nghĩa một  entity mà Swagger có thể sử dụng để xác định cấu trúc của đối tượng trả về. Dưới đây là cách cập nhật lớp ProductEntity trong tệp products.entity.ts:

// src/products/entities/product.entity.ts

import { ApiProperty } from '@nestjs/swagger';
import { Product } from '@prisma/client';

export class ProductEntity implements Product {
  @ApiProperty()
  id: number;

  @ApiProperty()
  name: string;

  @ApiProperty({ required: false, nullable: true })
  description: string | null;

  @ApiProperty()
  published: boolean;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;
}

Lớp ProductEntity bạn vừa định nghĩa là một implement của kiểu Product được tạo bởi Prisma Client, với các decorator @ApiProperty được thêm vào cho mỗi thuộc tính.

Bây giờ, bạn sẽ chú thích các route handlers trong controller bằng các kiểu phản hồi chính xác. NestJS cung cấp một bộ các decorator để làm điều này, như @ApiOkResponse, @ApiCreatedResponse, và @ApiNotFoundResponse, v.v.

// src/products/products.controller.ts

import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ProductEntity } from './entities/product.entity';

@Controller('products')
@ApiTags('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Post()
  @ApiCreatedResponse({ type: ProductEntity })
  create(@Body() createProductDto: CreateProductDto) {
    return this.productsService.create(createProductDto);
  }

  @Get(':id')
  @ApiOkResponse({ type: ProductEntity })
  findOne(@Param('id') id: string) {
    return this.productsService.findOne(+id);
  }

  @Get('drafts')
  @ApiOkResponse({ type: ProductEntity, isArray: true })
  findDrafts() {
    return this.productsService.findDrafts();
  }

  @Get()
  @ApiOkResponse({ type: ProductEntity, isArray: true })
  findAll() {
    return this.productsService.findAll();
  }

  @Patch(':id')
  @ApiOkResponse({ type: ProductEntity })
  update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {
    return this.productsService.update(+id, updateProductDto);
  }

  @Delete(':id')
  @ApiOkResponse({ type: ProductEntity })
  remove(@Param('id') id: string) {
    return this.productsService.remove(+id);
  }
}

Bạn đã thêm @ApiOkResponse cho các endpoint GET, PATCH, và DELETE, và @ApiCreatedResponse cho các endpoint POST. Thuộc tính type được sử dụng để chỉ định kiểu dữ liệu trả về. Tất cả các response decorator mà NestJS cung cấp đều có thể được tìm thấy trong tài liệu NestJS.

Bây giờ, Swagger sẽ hiển thị chi tiết rõ ràng về kiểu phản hồi cho tất cả các endpoint trên trang API, giúp cải thiện tài liệu và trải nghiệm người dùng khi tương tác với API.

Nếu bạn kiểm tra lại Swagger tại http://localhost:3000/api, bạn sẽ thấy các kiểu phản hồi được định nghĩa đầy đủ và chính xác cho từng endpoint. 🎉

Response Type

Tóm tắt và nhận xét:

Chúc mừng bạn! Bạn đã xây dựng thành công một REST API cơ bản bằng NestJS. Trong suốt hướng dẫn này, bạn đã:

  • Xây dựng một REST API với NestJS.
  • Tích hợp Prisma một cách mượt mà vào dự án NestJS.
  • Tài liệu hóa REST API của bạn bằng SwaggerOpenAPI.

Một bài học quan trọng từ hướng dẫn này là việc xây dựng REST API với NestJSPrisma rất dễ dàng. Đây là một stack vô cùng hiệu quả để nhanh chóng phát triển các ứng dụng backend có cấu trúc tốt, an toàn về kiểu dữ liệu (type-safe) và dễ bảo trì. 🚀

Bài viết được tham khảo từ một số nguồn:

  • Chuỗi bài hướng dẫn của tác giả Tasin Ishmam về xây dựng REST API, từ cơ bản đến validation, error handling, relational data, authentication…
  • Trang tài liệu của NestJS
  • Trang tài liệu của Prisma

Nếu có thời gian mình sẽ Việt hóa các bài viết tiếp theo của tác giả để các bạn có thể dễ dàng tạo nên một ứng dụng REST API BACKEND hoàn chỉnh. Hãy theo dõi và chờ đón nhé!

Subscribe
Notify of
5 Comments
Inline Feedbacks
View all comments
Aloda
6 days ago

Bài hướng dẫn khá dễ hiểu, cảm ơn tác giả. Mong chờ phần 2 😉

HT92
6 days ago

Bài viết rất chi tiết, cám ơn tác giả.

DCB
6 days ago

Bài viết quá chi tiết luôn, rất dễ hiểu, hóng phần 2, thanks a lot