☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Recipes

Prisma

Bản dịch từ docs.nestjs.com/recipes/prisma

Prisma là một ORM mã nguồn mở cho Node.js và TypeScript. Nó được dùng như một giải pháp thay thế cho việc viết SQL thuần, hoặc dùng các công cụ truy cập database khác như SQL query builder (ví dụ knex.js) hay ORM (như TypeORM và Sequelize). Prisma hiện hỗ trợ PostgreSQL, MySQL, SQL Server, SQLite, MongoDB và CockroachDB (bản Preview).

Mặc dù Prisma vẫn dùng được với JavaScript thuần, nó được thiết kế xoay quanh TypeScript và cung cấp mức độ an toàn kiểu dữ liệu (type-safety) vượt xa những gì các ORM khác trong hệ sinh thái TypeScript đảm bảo được. Bạn có thể xem so sánh chi tiết về mức độ type-safety giữa Prisma và TypeORM tại đây.

Lưu ý Nếu bạn muốn có cái nhìn tổng quan nhanh về cách Prisma hoạt động, hãy xem Quickstart hoặc đọc Introduction trong tài liệu chính thức. Ngoài ra còn có các ví dụ chạy sẵn cho REST và GraphQL trong repo prisma-examples.

Bắt đầu (Getting started)

Trong bài này, bạn sẽ học cách bắt đầu dùng NestJS với Prisma từ đầu. Bạn sẽ xây một ứng dụng NestJS mẫu với REST API có thể đọc và ghi dữ liệu vào database.

Để phục vụ mục đích hướng dẫn, bạn sẽ dùng database SQLite để tiết kiệm công cài đặt một database server. Bạn vẫn có thể theo hướng dẫn này dù đang dùng PostgreSQL hay MySQL — sẽ có hướng dẫn bổ sung cho các database đó ở đúng phần liên quan.

Lưu ý Nếu bạn đã có project sẵn và muốn chuyển sang dùng Prisma, hãy theo hướng dẫn thêm Prisma vào project đã có. Nếu bạn đang chuyển từ TypeORM, có thể đọc hướng dẫn Migrating from TypeORM to Prisma.

Tạo project NestJS

Để bắt đầu, cài NestJS CLI và tạo khung project bằng các lệnh sau:

$ npm install -g @nestjs/cli
$ nest new hello-prisma

Xem trang First steps để hiểu thêm về các file được tạo ra bởi lệnh này. Cũng lưu ý là bạn có thể chạy npm start để khởi động ứng dụng. REST API chạy tại http://localhost:3000/ hiện đang phục vụ một route duy nhất, được viết trong src/app.controller.ts. Trong suốt bài hướng dẫn này, bạn sẽ thêm các route khác để lưu và lấy dữ liệu về users và posts.

Thiết lập Prisma

Bắt đầu bằng cách cài Prisma CLI như một development dependency trong project:

$ cd hello-prisma
$ npm install prisma --save-dev

Ở các bước tiếp theo, chúng ta sẽ dùng Prisma CLI. Theo khuyến nghị (best practice), nên gọi CLI cục bộ bằng cách thêm tiền tố npx:

$ npx prisma
Bấm để xem nếu bạn dùng Yarn

Nếu bạn dùng Yarn, có thể cài Prisma CLI như sau:

$ yarn add prisma --dev

Sau khi cài xong, gọi CLI bằng tiền tố yarn:

$ yarn prisma

Giờ tạo cấu hình Prisma khởi đầu bằng lệnh init của Prisma CLI:

$ npx prisma init

Lệnh này tạo một thư mục prisma mới với nội dung sau:

  • schema.prisma: Khai báo kết nối database và chứa schema của database
  • prisma.config.ts: File cấu hình cho project của bạn
  • .env: Một file dotenv, thường dùng để lưu thông tin đăng nhập database trong một nhóm biến môi trường

Cấu hình đường dẫn output cho generator

Chỉ định path output cho Prisma client được sinh ra, bằng cách truyền --output ../src/generated/prisma khi chạy prisma init, hoặc khai báo trực tiếp trong Prisma schema:

generator client {
  provider        = "prisma-client"
  output          = "../src/generated/prisma"
}

Cấu hình module format

Đặt moduleFormat trong generator thành cjs:

generator client {
  provider        = "prisma-client"
  output          = "../src/generated/prisma"
  moduleFormat    = "cjs"
}

Lưu ý Cấu hình moduleFormat này là bắt buộc vì Prisma v7 mặc định sinh ra ES module, mà kiểu module này không tương thích với setup CommonJS mặc định của NestJS. Đặt moduleFormat thành cjs buộc Prisma sinh ra module dạng CommonJS thay vì ESM.

Ghi chú người dịch Đây là điểm khác với TypeORM hay Sequelize mà bạn đã quen: TypeORM không “generate” code, nó dùng decorator + reflection để đọc entity trực tiếp lúc runtime, còn Prisma sinh ra một package client hoàn chỉnh (từ schema) rồi bạn import package đó vào code. Vì thế nếu đổi schema mà quên chạy lại prisma generate, TypeScript vẫn build được nhưng các type của bạn sẽ bị lệch với schema thực tế — một cạm bẫy khá phổ biến khi mới chuyển từ TypeORM.

Cấu hình kết nối database

Kết nối database được khai báo trong block datasource của file schema.prisma. Theo mặc định, nó được đặt là postgresql, nhưng vì bạn dùng SQLite trong bài này, cần đổi field provider của block datasource thành sqlite:

datasource db {
  provider = "sqlite"
}

generator client {
  provider      = "prisma-client"
  output        = "../src/generated/prisma"
  moduleFormat  = "cjs"
}

Bây giờ, mở file .env và đổi biến môi trường DATABASE_URL thành:

DATABASE_URL="file:./dev.db"

Hãy đảm bảo bạn đã cấu hình ConfigModule, nếu không biến DATABASE_URL sẽ không được đọc từ .env.

Database SQLite chỉ là các file đơn giản; không cần server để dùng SQLite. Vì vậy thay vì cấu hình URL kết nối với host và port, bạn chỉ cần chỉ tới một file cục bộ, ở đây tên là dev.db. File này sẽ được tạo ở bước tiếp theo.

Bấm để xem nếu bạn dùng PostgreSQL, MySQL, MsSQL hoặc Azure SQL

Với PostgreSQL và MySQL, bạn cần cấu hình URL kết nối chỉ tới database server. Bạn có thể xem thêm về định dạng URL kết nối cần thiết tại đây.

PostgreSQL

Nếu bạn dùng PostgreSQL, cần chỉnh file schema.prisma và .env như sau:

schema.prisma

datasource db {
  provider = "postgresql"
}

generator client {
  provider = "prisma-client"
  output          = "../src/generated/prisma"
  moduleFormat  = "cjs"
}

.env

DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA"

Thay các placeholder viết hoa bằng thông tin đăng nhập database của bạn. Nếu bạn không rõ nên điền gì cho placeholder SCHEMA, giá trị mặc định thường là public:

DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public"

Nếu bạn muốn biết cách thiết lập một database PostgreSQL, hãy xem hướng dẫn thiết lập database PostgreSQL miễn phí trên Heroku.

MySQL

Nếu bạn dùng MySQL, cần chỉnh file schema.prisma và .env như sau:

schema.prisma

datasource db {
  provider = "mysql"
}

generator client {
  provider = "prisma-client"
  output          = "../src/generated/prisma"
  moduleFormat  = "cjs"
}

.env

DATABASE_URL="mysql://USER:PASSWORD@HOST:PORT/DATABASE"

Thay các placeholder viết hoa bằng thông tin đăng nhập database của bạn.

Microsoft SQL Server / Azure SQL Server

Nếu bạn dùng Microsoft SQL Server hoặc Azure SQL Server, cần chỉnh file schema.prisma và .env như sau:

schema.prisma

datasource db {
  provider = "sqlserver"
}

generator client {
  provider = "prisma-client"
  output          = "../src/generated/prisma"
  moduleFormat  = "cjs"
}

.env

Thay các placeholder viết hoa bằng thông tin đăng nhập database của bạn. Nếu không rõ nên điền gì cho placeholder encrypt, giá trị mặc định thường là true:

DATABASE_URL="sqlserver://HOST:PORT;database=DATABASE;user=USER;password=PASSWORD;encrypt=true"

Tạo hai bảng database bằng Prisma Migrate

Trong phần này, bạn sẽ tạo hai bảng mới trong database bằng Prisma Migrate. Prisma Migrate sinh ra các file migration SQL dựa trên định nghĩa data model khai báo (declarative) trong Prisma schema. Các file migration này có thể tùy biến hoàn toàn, cho phép bạn cấu hình thêm các tính năng khác của database, hoặc thêm các lệnh khác, ví dụ để seed dữ liệu.

Thêm hai model sau vào file schema.prisma:

model User {
  id    Int     @default(autoincrement()) @id
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int      @default(autoincrement()) @id
  title     String
  content   String?
  published Boolean? @default(false)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}

Với các Prisma model đã có, bạn có thể sinh file migration SQL và chạy chúng trên database. Chạy các lệnh sau trong terminal:

$ npx prisma migrate dev --name init

Lệnh prisma migrate dev sinh ra các file SQL và chạy trực tiếp trên database. Trong trường hợp này, các file migration sau được tạo trong thư mục prisma đã có:

$ tree prisma
prisma
├── dev.db
├── migrations
│   └── 20201207100915_init
│       └── migration.sql
└── schema.prisma
Bấm để xem các câu lệnh SQL được sinh ra

Các bảng sau được tạo trong database SQLite của bạn:

-- CreateTable
CREATE TABLE "User" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "email" TEXT NOT NULL,
    "name" TEXT
);

-- CreateTable
CREATE TABLE "Post" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" TEXT NOT NULL,
    "content" TEXT,
    "published" BOOLEAN DEFAULT false,
    "authorId" INTEGER,

    FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");

Cài đặt và sinh Prisma Client

Prisma Client là một database client type-safe, được sinh ra từ định nghĩa Prisma model của bạn. Nhờ cách tiếp cận này, Prisma Client có thể cung cấp các thao tác CRUD được thiết kế riêng cho model của bạn.

Để cài Prisma Client vào project, chạy lệnh sau trong terminal:

$ npm install @prisma/client

Sau khi cài xong, chạy lệnh generate để sinh ra type và Client cần cho project. Nếu bạn thay đổi schema, cần chạy lại lệnh generate để đồng bộ lại các type đó.

$ npx prisma generate

Ngoài Prisma Client, bạn cũng cần một driver adapter tương ứng với loại database bạn đang dùng. Với SQLite, bạn có thể cài driver @prisma/adapter-better-sqlite3.

npm install @prisma/adapter-better-sqlite3
Bấm để xem nếu bạn dùng PostgreSQL, MySQL, MsSQL, hoặc AzureSQL
  • Với PostgreSQL
npm install @prisma/adapter-pg
  • Với MySQL, MsSQL, AzureSQL:
npm install @prisma/adapter-mariadb

Dùng Prisma Client trong service của NestJS

Giờ bạn đã có thể gửi truy vấn database bằng Prisma Client. Nếu muốn biết thêm về cách xây truy vấn với Prisma Client, xem tài liệu API.

Khi thiết lập ứng dụng NestJS, bạn sẽ muốn bọc (abstract) Prisma Client API cho các truy vấn database vào trong một service. Để bắt đầu, tạo một PrismaService mới, chịu trách nhiệm khởi tạo PrismaClient và kết nối tới database.

Trong thư mục src, tạo file mới tên prisma.service.ts với nội dung sau:

import { Injectable } from '@nestjs/common';
import { PrismaClient } from './generated/prisma/client.js';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';

@Injectable()
export class PrismaService extends PrismaClient {
  constructor() {
    const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL });
    super({ adapter });
  }
}

Ghi chú người dịch So với Go, PrismaService extends PrismaClient gần giống việc bạn embed một struct rồi override constructor — mọi phương thức của PrismaClient (như prisma.user.findMany) tự động có sẵn trên PrismaService mà không cần viết lại. Đây cũng là lý do bạn có thể inject PrismaService như một provider Nest bình thường (@Injectable()), trong khi bản chất bên dưới vẫn là instance PrismaClient gốc của Prisma — không có “connect()” thủ công như bạn hay thấy khi tự quản lý pool kết nối bằng tay trong Express.

Tiếp theo, bạn có thể viết các service dùng để thực hiện truy vấn database cho model User và Post từ Prisma schema.

Vẫn trong thư mục src, tạo file mới tên user.service.ts với nội dung sau:

import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service.js';
import { User, Prisma } from 'generated/prisma';

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

  async user(
    userWhereUniqueInput: Prisma.UserWhereUniqueInput,
  ): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: userWhereUniqueInput,
    });
  }

  async users(params: {
    skip?: number;
    take?: number;
    cursor?: Prisma.UserWhereUniqueInput;
    where?: Prisma.UserWhereInput;
    orderBy?: Prisma.UserOrderByWithRelationInput;
  }): Promise<User[]> {
    const { skip, take, cursor, where, orderBy } = params;
    return this.prisma.user.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  async createUser(data: Prisma.UserCreateInput): Promise<User> {
    return this.prisma.user.create({
      data,
    });
  }

  async updateUser(params: {
    where: Prisma.UserWhereUniqueInput;
    data: Prisma.UserUpdateInput;
  }): Promise<User> {
    const { where, data } = params;
    return this.prisma.user.update({
      data,
      where,
    });
  }

  async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
    return this.prisma.user.delete({
      where,
    });
  }
}

Chú ý cách bạn dùng các type do Prisma Client sinh ra để đảm bảo các phương thức mà service của bạn expose ra có kiểu dữ liệu chính xác. Nhờ vậy bạn tiết kiệm được công viết type cho model và tạo thêm các file interface hay DTO riêng.

Giờ làm tương tự với model Post.

Vẫn trong thư mục src, tạo file mới tên post.service.ts với nội dung sau:

import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service.js';
import { Post, Prisma } from 'generated/prisma';

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

  async post(
    postWhereUniqueInput: Prisma.PostWhereUniqueInput,
  ): Promise<Post | null> {
    return this.prisma.post.findUnique({
      where: postWhereUniqueInput,
    });
  }

  async posts(params: {
    skip?: number;
    take?: number;
    cursor?: Prisma.PostWhereUniqueInput;
    where?: Prisma.PostWhereInput;
    orderBy?: Prisma.PostOrderByWithRelationInput;
  }): Promise<Post[]> {
    const { skip, take, cursor, where, orderBy } = params;
    return this.prisma.post.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  async createPost(data: Prisma.PostCreateInput): Promise<Post> {
    return this.prisma.post.create({
      data,
    });
  }

  async updatePost(params: {
    where: Prisma.PostWhereUniqueInput;
    data: Prisma.PostUpdateInput;
  }): Promise<Post> {
    const { data, where } = params;
    return this.prisma.post.update({
      data,
      where,
    });
  }

  async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> {
    return this.prisma.post.delete({
      where,
    });
  }
}

UsersService và PostsService hiện tại chỉ đang bọc lại các truy vấn CRUD có sẵn của Prisma Client. Trong một ứng dụng thực tế, đây cũng là nơi bạn thêm business logic cho ứng dụng. Ví dụ, bạn có thể có một phương thức tên updatePassword trong UsersService, chịu trách nhiệm đổi mật khẩu của một user.

Nhớ đăng ký các service mới này trong app module.

Triển khai các route REST API trong app controller chính

Cuối cùng, bạn sẽ dùng các service đã tạo ở phần trước để triển khai các route khác nhau của ứng dụng. Trong bài này, bạn sẽ đặt tất cả route vào class AppController đã có sẵn.

Thay nội dung file app.controller.ts bằng code sau:

import {
  Controller,
  Get,
  Param,
  Post,
  Body,
  Put,
  Delete,
} from '@nestjs/common';
import { UsersService } from './user.service.js';
import { PostsService } from './post.service.js';
import { User as UserModel, Post as PostModel } from 'generated/prisma';

@Controller()
export class AppController {
  constructor(
    private readonly userService: UsersService,
    private readonly postService: PostsService,
  ) {}

  @Get('post/:id')
  async getPostById(@Param('id') id: string): Promise<PostModel> {
    return this.postService.post({ id: Number(id) });
  }

  @Get('feed')
  async getPublishedPosts(): Promise<PostModel[]> {
    return this.postService.posts({
      where: { published: true },
    });
  }

  @Get('filtered-posts/:searchString')
  async getFilteredPosts(
    @Param('searchString') searchString: string,
  ): Promise<PostModel[]> {
    return this.postService.posts({
      where: {
        OR: [
          {
            title: { contains: searchString },
          },
          {
            content: { contains: searchString },
          },
        ],
      },
    });
  }

  @Post('post')
  async createDraft(
    @Body() postData: { title: string; content?: string; authorEmail: string },
  ): Promise<PostModel> {
    const { title, content, authorEmail } = postData;
    return this.postService.createPost({
      title,
      content,
      author: {
        connect: { email: authorEmail },
      },
    });
  }

  @Post('user')
  async signupUser(
    @Body() userData: { name?: string; email: string },
  ): Promise<UserModel> {
    return this.userService.createUser(userData);
  }

  @Put('publish/:id')
  async publishPost(@Param('id') id: string): Promise<PostModel> {
    return this.postService.updatePost({
      where: { id: Number(id) },
      data: { published: true },
    });
  }

  @Delete('post/:id')
  async deletePost(@Param('id') id: string): Promise<PostModel> {
    return this.postService.deletePost({ id: Number(id) });
  }
}

Controller này triển khai các route sau:

GET

  • /post/:id: Lấy một post theo id
  • /feed: Lấy tất cả post đã published
  • /filter-posts/:searchString: Lọc post theo title hoặc content

POST

  • /post: Tạo một post mới
    • Body:
      • title: String (bắt buộc): Tiêu đề của post
      • content: String (không bắt buộc): Nội dung của post
      • authorEmail: String (bắt buộc): Email của user tạo post
  • /user: Tạo một user mới
    • Body:
      • email: String (bắt buộc): Địa chỉ email của user
      • name: String (không bắt buộc): Tên của user

PUT

  • /publish/:id: Publish một post theo id

DELETE

  • /post/:id: Xóa một post theo id

Tổng kết

Trong bài này, bạn đã học cách dùng Prisma cùng với NestJS để triển khai một REST API. Controller triển khai các route của API gọi tới PrismaService, và service này lại dùng Prisma Client để gửi truy vấn tới database nhằm phục vụ nhu cầu dữ liệu của các request đến.

Ghi chú người dịch Kiến trúc 3 lớp Controller → Service → PrismaService ở đây gần giống pattern Repository trong Laravel hoặc layer “handler → usecase → repository” bạn hay thấy trong Go: controller chỉ lo nhận request/trả response, service chứa business logic, còn PrismaService đóng vai trò data-access layer thuần túy. Điểm khác với Laravel Eloquent là Prisma tách bạch hoàn toàn schema (khai báo) và client (sinh code), nên bạn không thể vừa định nghĩa model vừa gọi query trên cùng một class như Model::where(...).

Nếu bạn muốn tìm hiểu thêm về việc dùng NestJS với Prisma, hãy xem các tài nguyên sau:

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.