+11

Build CRUD REST API với NestJS, Docker, Swagger, Prisma

Build CRUD REST API với NestJS, Docker, Swagger, Prisma

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:

all-for-swagger-end-product.png

Ảnh chụp Swagger

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: Settting-nest.png

Đố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:

After-setting-up-nejst.png

Cấu trúc thư mục của dự án sau khi được tạo bằng Nest CLI

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ệp Hello 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 PostgreSQL
  • restart: always Đảm bảo container khởi động lại nếu nó dừng lại
  • environment Khai báo username và password cho database
  • volumes Gắn một thư mục để lưu trữ database ngay cả khi container bị dừng lại hoặc bị xóa
  • ports 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 field 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 file package.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/
  └─ 20240909065126_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:

-- CreateTable
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();
  });

Lưu ý: Nếu bạn code trong VSCode bị báo lỗi đỏ Delete CR eslint(prettier/prettier) thì bạn hãy mở file .eslintrc.js và thêm đoạn code bên dưới nhé:

module.exports = {
  ...
  rules: {
    // ...
  },
  'prettier/prettier': ['error', { endOfLine: 'auto' }],
}

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ức upsert, 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": {
    // ...
  },
  "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-09-09T16:21:09.133Z,
    updatedAt: 2024-09-09T16: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-09-09T16:21:09.155Z,
    updatedAt: 2024-09-09T16: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.module.ts
src/prisma/prisma.service.spec.ts
src/prisma/prisma.service.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. Swager-first-look.png

Giao diện ban đầu của Swagger UI sau khi thiết lập thành công.

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: Generating-CRUD.png

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: swagger-pplo--after-crating-crud.png

  • POST/recipe: Tạo một recipe mới
  • GET/recipe: Lấy danh sách các recipes.
  • GET/recipe/{id}: Lấy một recipe cụ thể bằng mã ID.
  • PATCH/recipe/{id}: Cập nhật một recipe cụ thể bằng mã ID.
  • DELETE/recipe/{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 constructor sau:

import { Injectable } from '@nestjs/common';
import { CreateRecipeDto } from './dto/create-recipe.dto';
import { UpdateRecipeDto } from './dto/update-recipe.dto';
import { PrismaService } from '../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. Chúng ta sẽ sử dụng PrismaService để thực hiện các hoạt động CRUD.

8-4. GET/recipe

Hãy bắt đầu hành trình tạo các API endpoints bằng cách định nghĩa endpoint GET/recipe. Endpoint này sẽ giúp truy xuất tất cả các recipes được lưu trữ trong database.

Trong file recipe.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()
findAll() {
  return this.recipeService.findAll();
}
  • Decorator @Get() ánh xạ phương thức này tới endpoint GET/recipe
  • Phương thức findAll sử dụng hàm findAll của recipeService nó sẽ lấy tất cả các recipes có trong 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àm findMany 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àm async 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:

get-all-recipe-swagger.png

Swagger UI hiển thị kết quả khi Fetch All Recipes

Như bạn thấy trong hình ảnh trên, endpoint GET/recipe 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ác công thức nấu ăn 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/recipe/{id}

Hãy tập trung vào endpoint GET/recipe/{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ả controllerservice

Đầu tiên bạn hãy mở file recipe.controller.ts. Tại đây, bạn sẽ thấy phương thức findOne được định nghĩa như sau:

@Get(':id')
findOne(@Param('id') id: string) {
  return this.recipeService.findOne(+id);
}
  • Decorator @Get(':id') sẽ ánh xạ tới endpoint GET/recipe/{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 recipe.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àm findUnique 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:

Get-by-id.png

Swagger UI hiển thị kết quả GET BY ID

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/recipe

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 endpoint POST/recipe
  • 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, ingredientsinstructions

Bạn hãy nhớ cài đặt thư viện này nhé.

npm install class-validator

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:

Creating-POST.png

Giao diện Swagger UI hiển thị quy trình tạo một recipe mớ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/recipe/{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/recipe/{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ả controllerservice.

Trong file recipe.controller.ts, tìm phương thức update. Phương thức này được ánh xạ tới endpoint PATCH/recipe/{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 endpoint PATCH/recipe/{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.

by-id3.png

Dữ liệu hiện tại của Recipe có ID = 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:

after-updating-id-3-response.png

Recipe sau khi cập nhật.

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/recipe/{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/recipe/{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 ở controllerservice.

Trong file recipe.controller.ts, chúng ta có phương thức remove. Phương thức này được ánh xạ tới endpoint DELETE/recipe/{id} :

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

Trong đoạn code này:

  • Decorator @Delete(':id') ánh xạ phương thức này tới endpoint DELETE/recipes/{id}
  • Phương thức remove chấp nhận một tham số id, tham số này được lấy từ tham số của route

Tiếp theo, hãy triển khai phương thức remove trong file recipe.service.ts bằng đoạn code sau:

remove(id: number) {
  return this.prisma.recipe.delete({
    where: { id },
  });
}

// other code ...
  • Phương thức remove sử dụng hàm delete 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.

delete by id

Hiển thị quá trình xóa một recipe cụ thể.

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

Source code: https://github.com/kentrung/crud-nestjs


All rights reserved

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