Chào mừng bạn đến với bài hướng dẫn cách tạo RESTful API với NestJS, Docker, Swagger và Prisma. Mục tiêu của tôi là hướng dẫn bạn cách xây dựng backend mạnh mẽ và hiệu quả, bất kể bạn là một dev dày dạn kinh nghiệm hay người mới bắt đầu bước chân vào thế giới lập trình.
Đây là những gì chúng ta sẽ xây dựng:
0. Bài toán
- Xây dựng một ứng dụng thêm, sửa, xóa các công thức nấu ăn (recipe)
- Mỗi Recipe sẽ có các trường (field) sau
- Title: Tên của recipe
- Description: Mô tả của recipe
- Ingredients: các nguyên liệu để làm
- Instructions: Hướng dẫn nấu ăn
1. Công Nghệ
Để xây dựng ứng dụng này, chúng ta sẽ tận dụng sức mạnh của các công cụ sau:
- NestJS: Node.js framework
- Prisma: An open-source database toolkit
- PostgreSQL: An open source object-relational database system.
- Docker: An open platform for developing, shipping, and running applications.
- Swagger: A tool for designing, building, and documenting RESTful APIs.
- TypeScript: A statically typed superset of JavaScript
Mỗi công nghệ này đóng một vai trò quan trọng trong việc tạo ra một ứng dụng mạnh mẽ, có thể mở rộng và bảo trì. Chúng ta sẽ đi sâu hơn vào từng vấn đề khi chúng ta tiếp tục.
2. Điều Kiện Tiên Quyết
Hướng dẫn này được thiết kế thân thiện với người mới bắt đầu, nhưng tôi đưa ra một số giả định về những gì bạn cần biết:
- Cơ bản về TypeScript
- Cơ bản về NestJS
- Docker
Nếu bạn không quen với những điều này, đừng lo lắng! Tôi sẽ hướng dẫn bạn.
3. Môi Trường Phát Triển
Trong hướng dẫn này, chúng ta sẽ sử dụng các công cụ sau:
- Node.js – Our runtime environment
- Docker – For containerizing our database
- Visual Studio Code – Our code editor
- PostgreSQL – Our database
- NestJS – Our Node.js framework
4. Thiết Lập Dự Án NestJS
Hãy bắt đầu bằng cách cài đặt NestJS CLI trên hệ thống của bạn:
npm i -g @nestjs/cli
Để khởi động một dự án mới, hãy thực hiện lệnh sau:
nest new recipe
Sau khi chạy lệnh này, bạn sẽ gặp một dấu nhắc giống như bên dưới:
Đối với dự án này, chúng ta sẽ chọn npm
. Sau khi bạn đã lựa chọn xong, CLI sẽ tiến hành thiết lập dự án.
Bây giờ bạn có thể mở dự án của mình trong VSCode (hoặc trình soạn thảo bạn thích). Bạn sẽ thấy các tập tin sau:
Hãy chia nhỏ cấu trúc dự án:
DIRECTORY/FILE | DESCRIPTION |
---|---|
recipe/ |
Thư mục gốc của dự án |
node_modules/ |
Chứa tất cả các gói npm cần thiết cho dự án |
src/ |
Chứa mã nguồn của ứng dụng |
src/app.controller.spec.ts |
File test của app.controller.ts |
src/app.controller.ts |
File app controller |
src/app.module.ts |
Root module của ứng dụng |
src/app.service.ts |
Chứa các service được sử dụng bởi app.controller.ts |
src/main.ts |
Entry point của ứng dụng |
test/ |
Folder chứa các file test |
test/app.e2e-spec.ts |
end-to-end tests cho app.controller.ts |
test/jest-e2e.json |
Cấu hình cho end-to-end tests |
README.md |
File readme của dự án |
nest-cli.json |
Cấu hình cho NestJS CLI |
package-lock.json |
Chứa phiên bản chính xác các gói npm được sử dụng trong dự án |
package.json |
Liệt kê các gói npm cần thiết cho dự án |
tsconfig.build.json |
Chứa các tùy chọn trình biên dịch TypeScript cho bản build |
Thư mục src
là trung tâm của ứng dụng, lưu trữ phần lớn code của chúng ta. NestJS CLI đã tạo tiền đề cho chúng ta với một số tệp chính:
src/app.module.ts
: Đây là root module của ứng dụng, đóng vai trò là điểm nối chính cho tất cả các module khác.src/app.controller.ts
: Tệp này chứa một controller cơ bản với một route duy nhất/
. Khi được truy cập, route này sẽ trả về thông điệpHello World!
src/main.ts
: Đây là cổng chính vào ứng dụng của chúng ta. Nó chịu trách nhiệm khởi động và chạy ứng dụng NestJS.
Để start dự án hãy thực hiện lệnh sau:
npm run start:dev
Lệnh command này kích hoạt live-reload development server. Nó theo dõi các file và nếu phát hiện bất kỳ sửa đổi nào, nó sẽ tự động biên dịch lại code và refresh server. Điều này đảm bảo rằng bạn có thể thấy các thay đổi của mình real-time, loại bỏ việc phải khởi động thủ công.
Để xác minh rằng máy chủ của bạn đã hoạt động, hãy truy cập http://localhost:3000/
trên web browser hoặc Postman. Bạn sẽ thấy một trang tối giản với thông điệp Hello World!
5. PostgreSQL
Để lưu trữ RESTful API các recipe, chúng ta sẽ sử dụng cơ sở dữ liệu (database) PostgreSQL. Docker sẽ giúp chúng ta chứa database này, đảm bảo quá trình thiết lập và thực thi suôn sẻ, bất kể môi trường.
Đầu tiên, hãy đảm bảo Docker được cài đặt trên hệ thống của bạn. Nếu không hãy làm theo hướng dẫn tại đây.
Tiếp theo, bạn sẽ cần tạo một file docker-compose.yml
Mở terminal và chạy lệnh sau:
touch docker-compose.yml
Lệnh này tạo một file docker-compose.yml
mới trong thư mục gốc của dự án. Viết code như sau:
version: '3.8'
services: postgres: image: postgres:13.5 restart: always environment: - POSTGRES_USER=recipe - POSTGRES_PASSWORD=RecipePassword volumes: - postgres:/var/lib/postgresql/data ports: - '5432:5432'
volumes: postgres:
Phân tích nhanh về cấu hình này:
image: postgres:13.5
Chọn Docker image cho PostgreSQLrestart: always
Đảm bảo container khởi động lại nếu nó dừng lạienvironment
Khai báo username và password cho databasevolumes
Gắn một thư mục để lưu trữ database ngay cả khi container bị dừng lại hoặc bị xóaports
Mở cổng 5432 trên cả host và container để truy cập cơ sở dữ liệu.
Lưu ý: Trước khi tiếp tục, hãy đảm bảo cổng 5432 không được sử dụng trên máy của bạn. Để khởi động container PostgreSQL, thực hiện lệnh sau trong thư mục gốc của dự án của bạn (và cũng đảm bảo rằng bạn đã mở ứng dụng Docker Desktop và nó đang chạy)
docker-compose up
Lệnh này sẽ khởi động container PostgreSQL và làm cho nó truy cập được cổng 5432 trên máy của bạn. Nếu mọi thứ diễn ra đúng theo kế hoạch, bạn sẽ thấy đầu ra tương tự như sau:
... | PostgreSQL init process complete; ready for start up.
postgres-1 |
postgres-1 | 2024-01-12 14:59:33.519 UTC [1] LOG: starting PostgreSQL 13.5 (Debian 13.5-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
postgres-1 | 2024-01-12 14:59:33.520 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
postgres-1 | 2024-01-12 14:59:33.520 UTC [1] LOG: listening on IPv6 address "::", port 5432
postgres-1 | 2024-01-12 14:59:33.526 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres-1 | 2024-01-12 14:59:33.533 UTC [62] LOG: database system was shut down at 2024-01-12 14:59:33 UTC
postgres-1 | 2024-01-12 14:59:33.550 UTC [1] LOG: database system is ready to accept connections
Lưu ý: Nếu bạn đóng cửa sổ terminal, điều này cũng sẽ dừng container. Để ngăn điều này xảy ra, bạn có thể chạy container ở chế độ detached
. Chế độ này cho phép container chạy vô thời hạn trong nền.
Để thực hiện điều này, thêm tùy chọn -d
vào cuối lệnh như sau:
docker-compose up -d
Để stop container, sử dụng lệnh command sau:
docker-compose down
Chúc mừng 🎉. Bạn hiện đã có database PostgreSQL riêng để thử nghiệm.
6. Prisma
Bây giờ chúng ta đã có database PostgreSQL, giờ bạn hãy tiếp tục thiết lập Prisma. Prisma là một công cụ cơ sở dữ liệu mã nguồn mở giúp việc tư duy về dữ liệu và cách tương tác với nó trở nên dễ dàng.
Prisma là một công cụ mạnh mẽ cung cấp một loạt các tính năng, bao gồm:
- Database Migrations: Prisma giúp bạn dễ dàng phát triển lược đồ cơ sở dữ liệu của mình theo thời gian mà không làm mất bất kỳ dữ liệu nào.
- Database Seeding: Prisma cho phép bạn tạo dữ liệu thử nghiệm (dummy data)
- Database Access: Prisma cung cấp các API mạnh mẽ cho việc truy cập database của bạn
- Database Schema Management: Prisma cho phép bạn định nghĩa database schema bằng Prisma Schema Language
- Database Querying: Prisma cung cấp các API mạnh mẽ để truy vấn cơ sở dữ liệu
- Database Relationships: Prisma cho phép bạn định nghĩa mối quan hệ giữa các bảng trong cơ sở dữ liệu
Bạn có thể tìm hiểu thêm về Prisma tại đây.
6-1. Khởi Tạo Prisma
Để bắt đầu với Prisma, chúng ta cần cài đặt Prisma CLI. CLI này cho phép chúng ta tương tác với cơ sở dữ liệu của mình, giúp thực hiện di chuyển cơ sở dữ liệu, tạo dữ liệu thử nghiệm và nhiều tính năng khác một cách dễ dàng.
Để cài đặt Prisma CLI, thực thi lệnh sau:
npm install prisma -D
Lệnh này cài đặt Prisma CLI như một devDependencies trong dự án của bạn thông qua cờ -D
.
Tiếp theo, khởi tạo Prisma trong dự án của bạn bằng cách thực thi lệnh sau:
npx prisma init
Điều này sẽ tạo ra một thư mục mới có tên là prisma và một tệp schema.prisma
bên trong. Đây là tệp cấu hình chính chứa database schema của bạn. Lệnh này cũng tạo ra một tệp .env
bên trong dự án của bạn.
6-2. Environment Variable
File .env
chứa các biến môi trường cần thiết để kết nối đến database. Hãy mở file này và thay thế nội dung bằng đoạn code sau:
DATABASE_URL="postgres://recipe:RecipePassword@localhost:5432/recipe"
Lưu ý: Nếu bạn đã thay đổi cổng trong file docker-compose.yml
thì hãy đảm bảo bạn cũng cập nhật cổng này trong biến môi trường DATABASE_URL
.
Biến môi trường này chứa chuỗi kết nối đến database trong container Docker.
6-3. Prisma Schema
File schema.prisma
chứa cấu trúc cho database của chúng ta. Nó được viết bằng Prisma Schema Language, một ngôn ngữ khai báo để định nghĩa cấu trúc cơ sở dữ liệu. File prisma/schema.prisma
là file cấu hình chính cho thiết lập Prisma. Nó định nghĩa database connection và Prisma Client generator.
generator client { provider = "prisma-client-js"
} datasource db { provider = "postgresql" url = env("DATABASE_URL")
}
Nó có ba thành phần chính:
- Generator: Phần này định nghĩa Prisma Client generator. Nó chịu trách nhiệm tạo ra Prisma Client, một API mạnh mẽ để truy cập cơ sở dữ liệu.
- Datasource: Phần này định nghĩa database connection. Nó chỉ định database provider và connection string. Nó sử dụng biến môi trường
DATABASE_URL
để kết nối đến cơ sở dữ liệu. - Model: Phần này định nghĩa database schema. Nó chỉ định các bảng, các trường.
6-4. Model Data
Bây giờ, khi đã thiết lập Prisma, chúng ta sẵn sàng để mô hình hóa dữ liệu của mình. Chúng ta cần định nghĩa một Recipe model. Model này sẽ có nhiều trường khác nhau.
Hãy mở file schema.prisma
và thêm đoạn code sau:
model Recipe { id Int @id @default(autoincrement()) title String @unique description String? ingredients String instructions String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
}
Đây là giải thích nhanh về mô hình này:
- id: Đây là khóa chính của Recipe model. Nó là một số nguyên tự động tăng để mỗi recipe đều có một ID riêng biệt. Nó có thuộc tính
@id
chỉ định đây là khóa chính (primary key). Thuộc tính@default(autoincrement())
yêu cầu Prisma tự động tăng giá trị này. - title: Đây là tên của recipe. Nó là một string và phải unique.
- description: Đây là mô tả của recipe. Nó là một string, dấu hỏi đằng sau thể hiện nó là tùy chọn có hoặc không có cũng được.
- ingredients: Đây là danh sách các thành phần nguyên liệu. Nó là một chuỗi chứa danh sách các thành phần được phân cách bằng dấu phẩy.
- instructions: Đây là danh sách các hướng dẫn để chuẩn bị công thức. Nó là một chuỗi chứa danh sách các hướng dẫn được phân cách bằng dấu phẩy.
- createdAt: Đây là ngày và giờ công thức được tạo ra. Nó được đặt mặc định là ngày và giờ hiện tại. Thuộc tính
@default(now())
yêu cầu Prisma đặt giá trị mặc định này. - updatedAt: Đây là ngày và giờ cuối cùng mà công thức được cập nhật. Nó tự động cập nhật khi công thức bị thay đổi.
6-5. Migrate Database
Bây giờ khi đã định nghĩa xong database schema, chúng ta đã sẵn sàng thực hiện quá trình migrate database. Điều này sẽ tạo ra các bảng và các trường trong cơ sở dữ liệu được định nghĩa trong file schema.prisma
Để thực hiện migrate database, bạn hãy chạy lệnh sau:
npx prisma migrate dev --name init
Lệnh này sẽ thực hiện ba việc:
- Save the migration: Prisma Migrate sẽ chụp lại một ảnh chụp nhanh của schema và xác định các lệnh SQL cần thiết để thực hiện migration. Prisma sẽ lưu file migration chứa các lệnh SQL này vào thư mục
prisma/migrations
vừa được tạo ra. - Execute the migration: Prisma Migrate sẽ thực thi các lệnh SQL trong file migration, tạo ra các bảng và các trường trong cơ sở dữ liệu theo như định nghĩa trong file
schema.prisma
- Generate Prisma Client: Prisma sẽ sinh ra Prisma Client dựa trên schema mới nhất của bạn. Nếu bạn chưa cài đặt Prisma Client, CLI sẽ tự động cài đặt nó cho bạn. Bạn sẽ thấy gói
@prisma/client
xuất hiện trong mục dependencies của filepackage.json
Prisma Client là một công cụ truy vấn được sinh tự động từ schema của bạn. Nó được điều chỉnh theo schema của Prisma và sẽ được sử dụng để gửi các truy vấn đến cơ sở dữ liệu.
Nếu mọi thứ hoạt động đúng kế hoạch, bạn sẽ thấy kết quả tương tự như sau:
The following migration(s) have been created and applied from new schema changes: migrations/ └─ 20220528101323_init/ └─ migration.sql Your database is now in sync with your schema.
...
✔ Generated Prisma Client (3.14.0 | library) to ./node_modules/@prisma/client in 31ms
Hãy kiểm tra file migration được tạo ra để hiểu hơn về những gì Prisma Migrate đang thực hiện đằng sau:
-- prisma/migrations/20220528101323_init/migration.sql CREATE TABLE "Recipe" ( "id" SERIAL NOT NULL, "title" TEXT NOT NULL, "description" TEXT, "ingredients" TEXT NOT NULL, "instructions" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "Recipe_pkey" PRIMARY KEY ("id")
); -- CreateIndex
CREATE UNIQUE INDEX "Recipe_title_key" ON "Recipe"("title");
File migration này chứa các lệnh SQL cần thiết để tạo ra bảng Recipe. Nó cũng chứa các lệnh SQL cần thiết để tạo trường title, là một trường unique. Điều này đảm bảo rằng trường title là duy nhất, ngăn chặn việc tạo ra các công thức trùng lặp.
6-6. Seed Database
Bây giờ khi chúng ta đã thực hiện migrate database, chúng ta sẽ tiến hành thêm dummy data để có thể kiểm tra ứng dụng mà không cần phải tạo thủ công các công thức nấu ăn.
Đầu tiên, tạo một file seed gọi là prisma/seed.ts
. File này sẽ chứa các dữ liệu mẫu và các truy vấn cần thiết để seed cơ sở dữ liệu của bạn.
Mở terminal và chạy lệnh sau:
touch prisma/seed.ts
Lệnh này sẽ tạo một file mới prisma/seed.ts
trong thư mục gốc của dự án. Tiếp theo mở file này lên và thêm đoạn code sau:
import { PrismaClient } from '@prisma/client'; // initialize Prisma Client
const prisma = new PrismaClient(); async function main() { // create two dummy recipes const recipe1 = await prisma.recipe.upsert({ where: { title: 'Spaghetti Bolognese' }, update: {}, create: { title: 'Spaghetti Bolognese', description: 'A classic Italian dish', ingredients: 'Spaghetti, minced beef, tomato sauce, onions, garlic, olive oil, salt, pepper', instructions: '1. Cook the spaghetti. 2. Fry the minced beef. 3. Add the tomato sauce to the beef. 4. Serve the spaghetti with the sauce.' } }); const recipe2 = await prisma.recipe.upsert({ where: { title: 'Chicken Curry' }, update: {}, create: { title: 'Chicken Curry', description: 'A spicy Indian dish', ingredients: 'Chicken, curry powder, onions, garlic, coconut milk, olive oil, salt, pepper', instructions: '1. Fry the chicken. 2. Add the curry powder to the chicken. 3. Add the coconut milk. 4. Serve the curry with rice.' } }); console.log({ recipe1, recipe2 });
} // execute the main function
main() .catch(e => { console.error(e); process.exit(1); }) .finally(async () => { // close Prisma Client at the end await prisma.$disconnect(); });
File này chứa dummy data và các câu query cần thiết để seed database. Giờ hãy break nó xem là gì:
import { PrismaClient } from '@prisma/client'
: Import một thư viện được sử dụng để gửi các truy vấn tới cơ sở dữ liệu.const prisma = new PrismaClient()
: Khởi tạo một instance của Prisma Client.async function main() { ... }
: Đây là hàm chính chứa dummy data và các truy vấn cần thiết để khởi tạo cơ sở dữ liệu.const recipe1 = await prisma.recipe.upsert({ ... })
: Tạo công thức nấu ăn 1. Nó sử dụng phương thứcupsert
, phương thức này sẽ tạo một recipe mới nếu nó không tồn tại, hoặc cập nhật nếu đã tồn tại.const recipe2 = await prisma.recipe.upsert({ ... })
: Tạo công thức nấu ăn 2.console.log({ recipe1, recipe2 })
: Log ra các recipe mới được tạo ra.main().catch((e) => { ... })
: Thực thi hàm main và bắt các lỗi xảy ra nếu có.wait prisma.$disconnect()
: Đóng kết nối Prisma Client khi hoàn tất.
Trước khi chúng ta có thể khởi tạo database, chúng ta cần thêm một script
vào file package.json
. Mở file này lên và thêm đoạn code sau vào phần "scripts":
// package.json // ... "scripts": { // ... }, "dependencies": { // ... }, "devDependencies": { // ... }, "jest": { // ... }, // pasting the prisma script here "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 sẵn như một dev dependencies trong package.json
.
Bây giờ, khi chúng ta đã định nghĩa xong script seed, bạn có thể khởi tạo database bằng cách thực hiện lệnh sau:
npx prisma db seed
Lệnh này sẽ khởi tạo database của bạn với dữ liệu mẫu được định nghĩa trong tệp prisma/seed.ts
. Nếu mọi thứ diễn ra đúng như kế hoạch, bạn sẽ thấy kết quả giống như sau:
Running seed command `ts-node prisma/seed.ts` ...
{ recipe1: { id: 1, title: 'Spaghetti Bolognese', description: 'A classic Italian dish', ingredients: 'Spaghetti, minced beef, tomato sauce, onions, garlic, olive oil, salt, pepper', instructions: '1. Cook the spaghetti. 2. Fry the minced beef. 3. Add the tomato sauce to the beef. 4. Serve the spaghetti with the sauce.', createdAt: 2024-01-12T16:21:09.133Z, updatedAt: 2024-01-12T16:21:09.133Z }, recipe2: { id: 2, title: 'Chicken Curry', description: 'A spicy Indian dish', ingredients: 'Chicken, curry powder, onions, garlic, coconut milk, olive oil, salt, pepper', instructions: '1. Fry the chicken. 2. Add the curry powder to the chicken. 3. Add the coconut milk. 4. Serve the curry with rice.', createdAt: 2024-01-12T16:21:09.155Z, updatedAt: 2024-01-12T16:21:09.155Z }
} The seed command has been executed.
Xin chúc mừng 🎉.
6-7. Prisma Service
Bây giờ chúng ta đã thiết lập xong Prisma, chúng ta sẵn sàng tạo Prisma Service. Service này sẽ hoạt động như một lớp bao quanh Prisma Client, giúp gửi các truy vấn đến database dễ dàng hơn.
Nest CLI cung cấp cho bạn một cách dễ dàng để tạo các modules và services trực tiếp từ CLI. Chạy lệnh sau trong terminal:
npx nest generate module prisma
npx nest generate service prisma
Lưu ý rằng lệnh generate
có thể được rút ngắn thành g
. Vì vậy, bạn cũng có thể chạy lệnh sau:
npx nest g module prisma
npx nest g service prisma
Lệnh này sẽ tạo một module mới có tên là prisma
và một service mới có tên là prisma
. Nó cũng sẽ nhập PrismaModule
vào AppModule
. Bạn sẽ thấy các file như sau:
src/prisma/prisma.service.spec.ts
src/prisma/prisma.service.ts
src/prisma/prisma.module.ts
Lưu ý: Trong một số trường hợp, bạn có thể cần phải khởi động lại server để các thay đổi có hiệu lực.
Tiếp theo, mở file prisma.service.ts
và thay thế nội dung bằng đoạn code sau:
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; @Injectable()
export class PrismaService extends PrismaClient {}
Service này là một lớp bao quanh Prisma Client giúp gửi các truy vấn đến database dễ dàng hơn. Nó cũng là một provider của NestJS, có nghĩa là nó có thể được tiêm (inject) vào các module khác.
Tiếp theo, mở file prisma.module.ts
và thay thế nội dung bằng đoạn code sau:
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service'; @Module({ providers: [PrismaService], exports: [PrismaService]
})
export class PrismaModule {}
Lưu ý: PrismaModule
là một NestJS module để import PrismaService
, nó sẽ được sử dụng trong các module khác. Cấu hình này cho phép tích hợp một cách liền mạch Prisma Service trên toàn dự án của bạn.
Chúc mừng 🎉! Bạn đã thiết lập thành công dịch vụ Prisma của mình.
Trước khi bắt đầu viết logic ứng dụng, hãy thiết lập Swagger. Swagger là công cụ tiêu chuẩn trong ngành để thiết kế, xây dựng và tài liệu hóa các API RESTful. Nó giúp cho các developer tạo tài liệu API một cách dễ dàng.
7. Swagger
Để cấu hình Swagger, chúng ta sẽ sử dụng package @nestjs/swagger
. Package này cung cấp một loạt các decorators và các methods được thiết kế đặc biệt để tạo tài liệu Swagger.
Để cài đặt gói này, hãy chạy lệnh sau:
npm install --save @nestjs/swagger swagger-ui-express
Lệnh này thêm package @nestjs/swagger
vào dependencies dự án của chúng ta. Nó cũng cài đặt swagger-ui-express
dùng để phục vụ Swagger UI.
Tiếp theo, hãy mở file main.ts
và thêm đoạn code sau:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; // Define the bootstrap function
async function bootstrap() { // Create a NestJS application instance by passing the AppModule to the NestFactory const app = await NestFactory.create(AppModule); // Use DocumentBuilder to create a new Swagger document configuration const config = new DocumentBuilder() .setTitle('Recipes API') // Set the title of the API .setDescription('Recipes API description') // Set the description of the API .setVersion('0.1') // Set the version of the API .build(); // Build the document // Create a Swagger document using the application instance and the document configuration const document = SwaggerModule.createDocument(app, config); // Setup Swagger module with the application instance and the Swagger document SwaggerModule.setup('api', app, document); // Start the application and listen for requests on port 3000 await app.listen(3000);
} // Call the bootstrap function to start the application
bootstrap();
Đoạn mã này khởi tạo Swagger và tạo tài liệu Swagger. Chúng ta hãy cùng nhau phân tích:
const config = new DocumentBuilder() ... .build()
: Đây là cách tạo một trình xây dựng tài liệu Swagger mới. Nó thiết lập tiêu đề, mô tả và phiên bản của tài liệu Swagger. Nó cũng xây dựng tài liệu Swagger.const document = SwaggerModule.createDocument(app, config)
: Đây là cách tạo tài liệu Swagger mới. Nó sử dụng trình xây dựng tài liệu Swagger để tạo tài liệu Swagger.SwaggerModule.setup('api', app, document)
: Đây là cách thiết lập Swagger UI. Nó sử dụng tài liệu Swagger để tạo Swagger UI.
Khi ứng dụng đang chạy, mở trình duyệt và điều hướng đến http://localhost:3000/api
. Bạn sẽ thấy Swagger UI như sau.
Bây giờ chúng ta đã thiết lập xong Swagger, chúng ta đã sẵn sàng bắt đầu xây dựng REST API của mình.
8. Triển khai CRUD cho Recipe Model
Trong phần này, chúng ta sẽ triển khai các hoạt động CRUD cho Recipe Model. Chúng ta sẽ bắt đầu bằng cách tạo các REST resources cho Recipe Model, sau đó thêm Prisma Client vào Recipe module và cuối cùng triển khai các hoạt động CRUD cho nó.
8-1. Tạo REST Resources
Chúng ta cần tạo các REST resources cho Recipe Model. Điều này sẽ tạo ra các boilerplate cho Recipe module, controller, service, và DTOs.
Bạn hãy thực hiện lệnh sau:
npx nest generate resource recipe
Lệnh này sẽ yêu cầu bạn chọn loại API bạn muốn tạo. Chúng ta sẽ chọn REST API. Dưới đây là hình ảnh tham khảo:
Lệnh này sẽ tạo ra các tệp sau:
CREATE src/recipe/recipe.controller.ts (959 bytes)
CREATE src/recipe/recipe.controller.spec.ts (596 bytes)
CREATE src/recipe/recipe.module.ts (264 bytes)
CREATE src/recipe/recipe.service.ts (661 bytes)
CREATE src/recipe/recipe.service.spec.ts (478 bytes)
CREATE src/recipe/dto/create-recipe.dto.ts (33 bytes)
CREATE src/recipe/dto/update-recipe.dto.ts (176 bytes)
CREATE src/recipe/entities/recipe.entity.ts (24 bytes)
UPDATE src/app.module.ts (385 bytes)
Nếu bạn mở lại trang Swagger API, bạn sẽ thấy một phần mới gọi là Recipe API. Phần này chứa các REST resources cho mô hình Recipe. Bạn sẽ thấy như sau:
POST/recipes
: Tạo một recipe mớiGET/recipes
: Lấy danh sách các recipes.GET/recipes/{id}
: Lấy một recipe cụ thể bằng mã ID.PATCH/recipes/{id}
: Cập nhật một recipe cụ thể bằng mã ID.DELETE/recipes/{id}
: Xoá một recipe cụ thể bằng mã ID.
8-2. Thêm PrismaClient
Đầu tiên, mở file recipe.module.ts
và thêm đoạn code sau:
import { Module } from '@nestjs/common';
import { RecipeService } from './recipe.service';
import { RecipeController } from './recipe.controller';
import { PrismaModule } from '../prisma/prisma.module'; @Module({ imports: [PrismaModule], controllers: [RecipeController], providers: [RecipeService]
})
export class RecipeModule {}
Chúng ta đã import PrismaModule
và thêm nó vào mảng imports. Điều này sẽ làm cho PrismaService
ở trạng thái sẵn sàng sử dụng ở RecipeService
Tiếp theo, mở file recipe.service.ts
và thêm đoạn code sau:
import { Body, Injectable, Post } from '@nestjs/common';
import { CreateRecipeDto } from './dto/create--recipe.dto';
import { UpdateRecipeDto } from './dto/update--recipe.dto';
import { PrismaService } from 'src/prisma/prisma.service'; @Injectable()
export class RecipesService { constructor(private readonly prisma: PrismaService) {} // rest of the code
}
Chúng ta đã định nghĩa PrismaService
là private property của class RecipesService
. Điều này cho phép chúng ta truy cập PrismaService
từ trong class RecipesService
. Chúng ta sẽ sử dụng PrismaService để thực hiện các hoạt động CRUD.
8-4. GET/recipes
Hãy bắt đầu hành trình tạo các API endpoints bằng cách định nghĩa endpoint GET/recipes
. Endpoint này sẽ giúp truy xuất tất cả các recipes được lưu trữ trong database.
Trong file recipes.controller.ts
, bạn sẽ thấy một phương thức tên là findAll
. Phương thức này đúng như tên gọi của nó chịu trách nhiệm lấy tất cả các recipes. Đây là cách chúng ta sẽ định nghĩa nó:
@Get()
async findAll() { return await this.recipeService.findAll();
}
- Decorator
@Get()
ánh xạ phương thức này tới endpointGET/recipes
- Phương thức
findAll
sử dụng hàmfindAll
củarecipeService
nó sẽ lấy tất cả các recipes có database.
Như bạn đã thấy, Controller
là trung tâm của ứng dụng. Trong trường hợp này, chúng ta sẽ triển khai phương thức findAll
để lấy tất cả các recipe từ database. Để làm được điều này, chúng ta sẽ sử dụng các dịch vụ của Prisma trong file recipe.service.ts
. Khi mở file này lên bạn sẽ thấy code như sau:
findAll() { return `This action returns all recipe`; }
Thay thế bằng đoạn code sau:
async findAll() { return this.prisma.recipe.findMany();
}
- Phương thức
findAll
sử dụng hàmfindMany
của Prisma để lấy tất cả các recipe từ database. - Từ khóa
await
không cần thiết ở đây vì hàmasyn
c tự động bọc giá trị trả về trong một Promise.
Như vậy chúng ta đã triển khai thành công phương thức findAll
để lấy tất cả các recipe.
Vì chúng ta đã có dữ liệu mẫu trong database, mở Swagger sẽ cho phép chúng ta truy xuất tất cả các recipe. Đây là những gì bạn có thể mong đợi:
Như bạn thấy trong hình ảnh trên, endpoint GET/recipes
của chúng ta hoạt động đúng như mong đợi, truy xuất thành công tất cả các recipe từ database.
Đây là một cột mốc quan trọng trong hành trình xây dựng hệ thống quản lý công thức nấu ăn mạnh mẽ của chúng ta. Hãy tiếp tục và thêm một số tính năng khác.
8-5. GET/recipes/{id}
Hãy tập trung vào endpoint GET/recipes/{id}
, endpoint này sẽ truy xuất một recipe cụ thể dựa trên ID của nó. Để triển khai, chúng ta cần chỉnh sửa cả controller
và service
Đầu tiên bạn hãy mở file recipes.controller.ts
. Tại đây, bạn sẽ thấy phương thức findOne
được định nghĩa như sau:
@Get(':id')
async findOne(@Param('id') id: string) { return await this.recipeService.findOne(+id);
}
- Decorator
@Get(':id')
sẽ ánh xạ tới endpointGET/recipes/{id}
- Phương thức
findOne
chấp nhận một tham sốid
có kiểu là string được lấy ra từ route parameters.
Tiếp theo hãy chuyển sự chú ý sang file recipes.service.ts
. Bạn sẽ thấy đoạn code như sau:
findOne(id: number) { return `This action returns a #${id} recipe`; }
Chúng ta sẽ thay thế phương thức này bằng một phương thức thực sự để truy xuất một recipe dựa trên ID của nó:
findOne(id: number) { return this.prisma.recipe.findUnique({ where: { id }, });
}
- Phương thức
findOne
nhận một tham sốid
và sử dụng hàmfindUnique
của Prisma để truy xuất recipe với ID tương ứng. - Với những thay đổi này, bạn đã có thể truy xuất các recipe riêng lẻ dựa theo ID.
Để xem tính năng này hoạt động, hãy điều hướng đến trang Swagger của bạn. Dưới đây là hình ảnh minh họa về những gì bạn có thể mong đợi:
Sau khi đạt được cột mốc này, chúng ta đã sẵn sàng tiến hành tạo các recipe mới, bổ sung vào những recipe hiện có trong database của chúng ta.
8-6. POST/recipes
CLI của NestJS đã tạo ra một phương thức create
khi chúng ta tạo tài nguyên cho mô hình Recipe. Bây giờ, chúng ta cần triển khai logic cho phương thức này trong file recipe.service.ts
Đầu tiên, hãy xem qua phương thức create
trong file này. Chúng ta sẽ thấy đoạn code như sau:
@Post()
create(@Body() createRecipeDto: CreateRecipeDto) { return this.recipesService.create(createRecipeDto);
}
// other code ...
- Decorator
@Post()
ánh xạ phương thức này tới endpointPOST/recipes
- Phương thức
create
chấp nhận một tham sốcreateRecipeDto
, được lấy từ request body.
CLI của NestJS đã chu đáo cung cấp cho chúng ta các tệp DTO (Data Transfer Object) trong thư mục recipe
. Một trong số đó, CreateRecipeDto
sẽ là công cụ chúng ta chọn để xác thực dữ liệu nhận được từ client.
Sơ lược về DTO
: Nếu bạn mới làm quen với khái niệm DTO, chúng thực chất là các đối tượng dùng để truyền dữ liệu giữa các process. Trong ngữ cảnh của ứng dụng này, chúng ta sẽ sử dụng DTO để đảm bảo rằng dữ liệu nhận được phù hợp với mong đợi (expectation) của chúng ta. Nếu bạn muốn tìm hiểu sâu hơn về DTO, hãy tham khảo hướng dẫn chi tiết ở đây
Bây giờ, hãy triển khai phương thức create
trong file recipe.service.ts
để tương tác với cơ sở dữ liệu.
Nhưng trước khi tiến hành, hãy tận dụng sức mạnh của thư mục DTO (Data Transfer Object) mà CLI của NestJS đã tạo ra để mô hình hóa dữ liệu.
Class CreateRecipeDto
như được hiển thị dưới đây, là một ví dụ điển hình về DTO. Nó được thiết kế để xác thực dữ liệu nhận được từ phía client, đảm bảo rằng dữ liệu này phù hợp với expect của chúng ta.
import { IsString, IsOptional } from 'class-validator'; export class CreateRecipeDto { @IsString() title: string; @IsOptional() @IsString() description?: string; @IsString() ingredients: string; @IsString() instructions: string;
}
Trong class này, chúng ta đang sử dụng package class-validator
để xác thực dữ liệu. Package này cung cấp một loạt các decorators như IsString
, IsOptional
, mà chúng ta sử dụng để xác thực các trường title
, description
, ingredients
và instructions
Với cấu hình này, chúng ta có thể tự tin đảm bảo rằng các trường này sẽ luôn là chuỗi ký tự, trong đó description là trường optional (có hoặc không có cũng được).
Bây giờ, hãy triển khai phương thức create
trong file recipe.service.ts
để tương tác với cơ sở dữ liệu. Khi bạn mở file này lên bạn sẽ thấy một đoạn code như sau:
create(createRecipeDto: CreateRecipeDto) { return 'This action adds a new recipe'; }
Thay thế bằng đoạn code sau:
create(createRecipeDto: CreateRecipeDto) { return this.prisma.recipe.create({ data: createRecipeDto, });
}
Phương thức create
sử dụng hàm create
của Prisma để thêm một recipe mới vào cơ sở dữ liệu. Dữ liệu cho recipe mới được cung cấp bởi createRecipeDto
Với những thay đổi này, bạn có thể tạo ra các recipe mới trên trang Swagger của mình. Đây là những gì bạn có thể mong đợi:
Như được minh họa trong hình trên, chúng ta đã thành công thêm recipe thứ ba vào bộ sưu tập của mình. Điều này chứng minh tính hiệu quả của phương thức POST trong việc tạo các recipe mới.
8-7. PATCH/recipes/{id}
Sau khi đã triển khai các endpoint để tạo và truy xuất công thức nấu ăn (recipe), giờ chúng ta sẽ tập trung vào việc cập nhật một recipe nào đó. Chúng ta sẽ triển khai endpoint PATCH/recipes/{id}
, dùng để cập nhật một recipe cụ thể dựa trên ID của nó. Điều này yêu cầu các thay đổi ở cả controller
và service
.
Trong file recipes.controller.ts
, tìm phương thức update
. Phương thức này được ánh xạ tới endpoint PATCH/recipes/{id}
@Patch(':id')
update(@Param('id') id: string, @Body() updateRecipeDto: UpdateRecipeDto) { return this.recipesService.update(+id, updateRecipeDto);
}
// other code ...
- Decorator
@Patch(':id')
ánh xạ phương thức này tới endpointPATCH/recipes/{id}
- Phương thức
update
chấp nhận hai tham số:id
(được lấy từ các tham số của route) vàupdateRecipeDto
(được lấy từ request body).
Tiếp theo, hãy triển khai phương thức update
trong file recipe.service.ts
. Khi mở file này lên bạn sẽ thấy đoạn code như sau:
update(id: number, updateRecipeDto: UpdateRecipeDto) { return `This action updates a #${id} recipe`; } // other code ...
Thay thế bằng đoạn code sau:
update(id: number, updateRecipeDto: UpdateRecipeDto) { return this.prisma.recipe.update({ where: { id }, data: updateRecipeDto, });
}
Phương thức update
sử dụng hàm update
của Prisma để cập nhật recipe trong cơ sở dữ liệu. Mệnh đề where
xác định recipe cần cập nhật (dựa trên id
), mệnh đề data
chỉ định dữ liệu mới cho recipe (được cung cấp bởi updateRecipeDto
)
Với những thay đổi này chúng ta đã mở khóa khả năng cập nhật các recipe riêng lẻ dựa theo id
của chúng.
Hãy thử tính năng mới này bằng cách cập nhật recipe có id
bằng 3.
Như được mô tả ở trên, đây là dữ liệu hiện có của recipe mà chúng ta sắp cập nhật.
Sau khi thực hiện thao tác cập nhật, recipe của chúng ta sẽ biến đổi như sau:
Như bạn thấy, thao tác cập nhật đã thành công và recipe đã được thay đổi, thể hiện hiệu quả của tính năng mới được triển khai.
Giờ hãy chuyển sự chú ý sang việc xóa các công thức nấu ăn thôi.
8-8. DELETE/recipes/{id}
Sau khi đã định nghĩa xong các endpoints GET
, POST
, PATCH
, nhiệm vụ tiếp theo là triển khai endpoint DELETE/recipes/{id}
. Endpoint này cho phép chúng ta xóa một recipe nào đó bằng mã ID. Cũng như các endpoint trước, chúng ta cần thực hiện thay đổi ở controller
và service
.
Trong file recipes.controller.ts
, chúng ta có phương thức remove
. Phương thức này được ánh xạ tới endpoint DELETE/recipes/{id}
:
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number) { return await this.recipesService.remove(id);
}
Trong đoạn code này:
- Decorator
@Delete(':id')
ánh xạ phương thức này tới endpointDELETE/recipes/{id}
- Phương thức
remove
chấp nhận một tham sốid
, tham số này được lấy từ các tham số của route và được chuyển đổi thànhNumber
bằng cách sử dụngParseIntPipe
Tiếp theo, hãy triển khai phương thức remove
trong file recipe.service.ts
.
@Delete(':id') remove(@Param('id') id: string) { return this.recipeService.remove(+id); } // other code ...
Hãy thay thế phương thức remove
bằng đoạn code sau:
async remove(id: number) { return await this.prisma.recipe.delete({ where: { id }, });
} // other code ...
- Phương thức
remove
sử dụng hàmdelete
của Prisma để xóa recipe có ID tương ứng khỏi cơ sở dữ liệu. - Với những thay đổi này, bạn có thể xóa các recipe riêng lẻ bằng ID của chúng. Kiểm tra trang Swagger để xem tài liệu API đã được cập nhật.
9. Tổng kết
Trong tài liệu hướng dẫn này, chúng ta đã cùng nhau đi qua quá trình xây dựng một REST API sử dụng NestJS và Prisma.
Chúng ta đã bắt đầu bằng việc thiết lập một dự án NestJS, cấu hình cơ sở dữ liệu PostgreSQL bằng Docker và tích hợp Prisma.
Sau đó, chúng ta đã đi sâu vào phần cốt lõi của ứng dụng, tạo một mô hình Recipe và thực hiện các thao tác CRUD cho nó. Điều này bao gồm việc tạo ra các routes RESTful, tích hợp Prisma Client vào Recipe Service và xây dựng logic cho từng thao tác.
Hướng dẫn này sẽ là nền tảng vững chắc cho các dự án tương lai của bạn. Hãy thoải mái mở rộng nó, thêm nhiều tính năng và chức năng phù hợp với nhu cầu của bạn. Cảm ơn bạn đã theo dõi, chúc bạn lập trình vui vẻ!
Tham khảo: https://www.freecodecamp.org/news/build-a-crud-rest-api-with-nestjs-docker-swagger-prisma