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 databaseprisma.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 theoid/feed: Lấy tất cả post đã published/filter-posts/:searchString: Lọc post theotitlehoặccontent
POST
/post: Tạo một post mới- Body:
title: String(bắt buộc): Tiêu đề của postcontent: String(không bắt buộc): Nội dung của postauthorEmail: String(bắt buộc): Email của user tạo post
- Body:
/user: Tạo một user mới- Body:
email: String(bắt buộc): Địa chỉ email của username: String(không bắt buộc): Tên của user
- Body:
PUT
/publish/:id: Publish một post theoid
DELETE
/post/:id: Xóa một post theoid
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: