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.
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
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ụngpostgres 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 ướchost_port:container_port
. Trong trường hợp này, bạn đang ánh xạ cổng5432
của máy chủ sang cổng5432
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
npm install -D prisma
npx prisma init
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
//.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ườngDATABASE_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
DateTime
làcreatedAt
vàupdatedAt
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:
- 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. - 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.
- 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ệppackage.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:
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:
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 create vì upsert 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:
Lưu ý: Bạn có thể đọc thêm về seeding ở Prisma Docs
Tạo Prisma service
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ề services và modules.
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.ts
và prisma.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.
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:
- What name would you like to use for this resource (plural, e.g., “users”)? Bạn nhập
products
. - What transport layer do you use? Chọn
REST API
. - 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.
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ả.
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ả.
Đị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.
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.
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. 🎉
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 Swagger và OpenAPI.
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 NestJS và Prisma 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é!
Bài hướng dẫn khá dễ hiểu, cảm ơn tác giả. Mong chờ phần 2 😉
Cảm ơn bạn đã feedback, động lực để mình ra phần tiếp theo ^^
Bài viết rất chi tiết, cám ơn tác giả.
Cảm ơn bạn đã review bài viết của mình 😀 😀
Bài viết quá chi tiết luôn, rất dễ hiểu, hóng phần 2, thanks a lot