Xây dựng REST API quản lý users với Node.js/TypeScript (Phần 3: Prisma, Authentication và Automated Tests với Vitest)
Trong hai phần trước, chúng ta đã:
- Phần 1: Thiết lập cấu trúc dự án với Express, chia routes/module rõ ràng theo tính năng.
- Phần 2: Cấu trúc hóa ứng dụng theo các lớp DAO → Service → Controller → Middleware và sử dụng argon2 để hash mật khẩu khi lưu.
Ở Phần 3, chúng ta sẽ hoàn chỉnh API bằng cách:
- Kết nối với cơ sở dữ liệu thật sử dụng Prisma.
- Triển khai Authentication bằng JWT bảo mật endpoint.
- Tích hợp Automated Tests với Vitest + Supertest để đảm bảo endpoint hoạt động đúng.
I. Cài đặt Dependencies
Trước hết, bạn chạy lệnh sau để thêm những thư viện quan trọng:
npm install @prisma/client prisma dotenv jsonwebtoken
npm install --save-dev vitest supertest @types/jsonwebtoken @types/supertest
Giải thích nhanh:
- @prisma/client & prisma: ORM giúp định nghĩa model và thao tác CRUD với database một cách type-safe.
- dotenv: Load biến môi trường từ file .env.
- jsonwebtoken: Tạo và xác thực token bảo mật.
- vitest, supertest: Cho phép viết thử nghiệm API mô phỏng client thực thụ.
Lưu ý: Chúng ta vẫn giữ argon2 để mã hoá mật khẩu — bạn đã thêm ở phần trước.
II. Prisma & Cấu hình Database
2.1. Khởi tạo Prisma
Chạy:
npx prisma init
Lệnh này tạo thư mục prisma/ và file schema.prisma để định nghĩa dữ liệu.
2.2. Định nghĩa Model User
Trong file prisma/schema.prisma, bạn viết:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
firstName String?
lastName String?
permissionLevel Int? @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Mỗi model trong Prisma tương ứng một bảng trong database. Chúng ta dùng UUID cho ID để tránh trùng lặp và dễ mở rộng về sau.
2.3. Cấu hình Biến Môi Trường
Tạo file .env tại root:
DATABASE_URL="mysql://USER:PASSWORD@localhost:3306/users_db"
JWT_SECRET="your-super-secret"
– DATABASE_URL là chuỗi kết nối đến MySQL/MariaDB.
– JWT_SECRET dùng để ký và xác thực JWT.
2.4. Prisma Client để tương tác DB
Tạo file lib/prisma.ts:
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
Với Prisma Client này, bạn sẽ gọi các method như .findUnique(), .create()… tương tự như ORM thông thường.
2.5. Tạo Migration
Chạy:
npx prisma migrate dev --name init
Lệnh này:
- Tạo migration đầu tiên với tên “init”.
- Tạo bảng User trong database dựa trên schema đã định nghĩa.
III. Refactor UsersDAO sang Prisma
Trong phần trước, Users DAO lưu trong RAM. Giờ chúng ta chuyển sang thao tác với database thật.
import { prisma } from "../../lib/prisma";
import { PostUserDto } from "../dtos/post.user.dto";
import { PatchUserDto } from "../dtos/patch.user.dto";
import { PutUserDto } from "../dtos/put.user.dto";
class UsersDao {
async addUser(user: PostUserDto) {
const created = await prisma.user.create({
data: user,
select: { id: true },
});
return created.id;
}
async getUsers(limit = 20, page = 1) {
const skip = (page - 1) * limit;
return prisma.user.findMany({
skip,
take: limit,
orderBy: { createdAt: "desc" },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
permissionLevel: true,
createdAt: true,
updatedAt: true,
},
});
}
async getUserById(userId: string) {
return prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
permissionLevel: true,
createdAt: true,
updatedAt: true,
},
});
}
async putUserById(userId: string, user: PutUserDto) {
return prisma.user.update({
where: { id: userId },
data: user,
select: { id: true },
});
}
async patchUserById(userId: string, user: PatchUserDto) {
return prisma.user.update({
where: { id: userId },
data: user,
select: { id: true },
});
}
async removeUserById(userId: string) {
await prisma.user.delete({ where: { id: userId } });
return `${userId} removed`;
}
}
export default new UsersDao();
Đoạn code trên chuyển toàn bộ thao tác CRUD user sang Prisma Client. Việc dùng select giúp chúng ta chỉ trả những trường cần thiết (không trả password cho client).
IV. Authentication: JWT & Middleware
4.1 Định nghĩa Route
import express from "express";
import AuthController from "./controllers/auth.controller";
export class AuthRoutes {
app: express.Application;
constructor(app: express.Application) {
this.app = app;
this.configureRoutes();
}
configureRoutes() {
this.app.post("/auth/login", AuthController.login);
return this.app;
}
}
Route này chỉ xử lý việc đăng nhập và trả JWT, không yêu cầu token trước.
4.2 Controller & Service
Controller gọi service để xác thực email/password và trả token.
import { Request, Response } from "express";
import AuthService from "../services/auth.service";
class AuthController {
async login(req: Request, res: Response) {
const { email, password } = req.body;
const token = await AuthService.login(email, password);
if (!token) {
return res.status(401).json({ error: "Invalid credentials" });
}
res.json({ token });
}
}
export default new AuthController();
import { prisma } from "../../lib/prisma";
import argon2 from "argon2";
import jwt from "jsonwebtoken";
export default class AuthService {
static async login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) return null;
const ok = await argon2.verify(user.password, password);
if (!ok) return null;
const payload = {
userId: user.id,
email: user.email,
permissionLevel: user.permissionLevel,
};
return jwt.sign(payload, process.env.JWT_SECRET as string, {
expiresIn: "1h",
});
}
}
Đầu tiên service truy vấn user theo email. Nếu tìm thấy và mật khẩu khớp (argon2.verify), chúng ta tạo payload và ký JWT để trả về client.
4.3 Middleware xác thực token
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
export interface AuthRequest extends Request {
user?: { userId: string; email: string; permissionLevel?: number };
}
export default class AuthMiddleware {
static authenticateToken(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers["authorization"];
if (!authHeader) {
return res.status(401).json({ error: "No token provided" });
}
const parts = authHeader.split(" ");
if (parts.length !== 2) {
return res.status(401).json({ error: "Invalid token format" });
}
const token = parts[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET as string) as {
userId: string;
email: string;
permissionLevel?: number;
};
req.user = payload;
next();
} catch {
return res.status(401).json({ error: "Invalid token" });
}
}
}
Middleware sẽ bắt token từ Authorization, verify signature bằng JWT_SECRET và gắn payload vào req.user để sử dụng sau này.
4.4 Bảo vệ route users
Trong file users/users.routes.config.ts, thêm middleware:
this.app.use("/users", AuthMiddleware.authenticateToken);
Điều này khiến mọi endpoint trong nhóm /users yêu cầu token hợp lệ trước khi truy cập.
V. Hoàn thiện UsersController
import express from "express";
import usersService from "../services/users.service";
import argon2 from "argon2";
class UsersController {
async listUsers(req: express.Request, res: express.Response) {
const limit = Number(req.query.limit ?? 20);
const page = Number(req.query.page ?? 1);
const users = await usersService.list(limit, page);
res.status(200).send(users);
}
async getUserById(req: express.Request, res: express.Response) {
const user = await usersService.readById(req.params.userId);
res.status(200).send(user);
}
async createUser(req: express.Request, res: express.Response) {
req.body.password = await argon2.hash(req.body.password);
const userId = await usersService.create(req.body);
res.status(201).send({ id: userId });
}
async patch(req: express.Request, res: express.Response) {
if (req.body.password) {
req.body.password = await argon2.hash(req.body.password);
}
await usersService.patchById(req.params.userId, req.body);
res.status(204).send();
}
async put(req: express.Request, res: express.Response) {
req.body.password = await argon2.hash(req.body.password);
await usersService.putById(req.params.userId, req.body);
res.status(204).send();
}
async removeUser(req: express.Request, res: express.Response) {
await usersService.deleteById(req.params.userId);
res.status(204).send();
}
}
export default new UsersController();
Mỗi method xử lý tương tự REST chuẩn — có hash password khi tạo/update và trả status code đúng theo chuẩn HTTP.
VI. Automated Tests với Vitest + Supertest
6.1 Export App
Trong file app.ts, đảm bảo bạn export default app để Supertest có thể gọi API trực tiếp mà không cần listen port.
6.2 Thiết lập Vitest
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
setupFiles: ["dotenv/config"],
include: ["tests/**/*.test.ts"],
},
});
Cấu hình này giúp load biến môi trường trước khi test và chạy trong môi trường Node (không phải browser).
6.3 Viết Tests
import { describe, it, beforeAll, afterAll, expect } from "vitest";
import request from "supertest";
import app from "../app";
import { prisma } from "../lib/prisma";
import argon2 from "argon2";
describe("Users API", () => {
beforeAll(async () => {
await prisma.$connect();
const email = "[email protected]";
const existed = await prisma.user.findUnique({ where: { email } });
if (!existed) {
await prisma.user.create({
data: {
email,
password: await argon2.hash("123123123"),
firstName: "Test",
lastName: "User",
permissionLevel: 0,
},
});
}
});
afterAll(async () => {
await prisma.$disconnect();
});
let token = "";
it("should login and return JWT", async () => {
const res = await request(app)
.post("/auth/login")
.send({ email: "[email protected]", password: "123123123" });
expect(res.status).toBe(200);
expect(res.body.token).toBeTruthy();
token = res.body.token;
});
it("should get users with auth token", async () => {
const res = await request(app)
.get("/users")
.set("Authorization", `Bearer ${token}`);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
it("should reject request without token", async () => {
const res = await request(app).get("/users");
expect(res.status).toBe(401);
});
});
Đoạn test này:
- Kết nối DB trước khi test.
- Tạo user test nếu chưa tồn tại.
- Test login, test /users với và không với token.
6.4 Chạy Tests
Trong package.json:
{
"scripts": {
"test": "vitest run"
}
}
Rồi chạy:
npm test
VII. Kiểm thử nhanh bằng curl
Login lấy token:
curl --request POST 'localhost:3000/auth/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"password": "123123123"
}'
Gọi route users với token:
curl --request GET 'localhost:3000/users' \
--header 'Authorization: Bearer <YOUR_TOKEN>'
VIII. Kết luận
Vậy là chúng ta đã hoàn chỉnh được 1 API với Node.js/TypeScript, đồng thời biết cách tạo được các test case để kiểm thử thành quả của mình. Hy vọng chuỗi bài viết này sẽ giúp ích được cho các bạn trong quá trình làm việc.
Cảm ơn vì đã đồng hành cùng bài viết nhé.
![]() | Vắn Quang Quí PHP Developer |














