Chia sẻ
//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)

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

ỨNG TUYỂN







    Chế độ phúc lợi

    CHÍNH SÁCH LƯƠNG & THƯỞNG

    Thấu hiểu tâm tư nguyện vọng của nhân viên, công ty Rivercrane Việt Nam đặc biệt thiết lập chế độ xét tăng lương định kỳ 2lần/năm. Xét đánh giá vào tháng 06 và tháng 12 hàng năm và thay đổi lương vào tháng 01 và tháng 07 hàng năm. Ngoài ra, nhân viên còn được thưởng thành tích định kỳ cho các cá nhân xuất sắc trong tháng, năm.

    CHẾ ĐỘ ĐÀO TẠO TẠI NHẬT

    Luôn luôn mong muốn các kỹ sư và nhân viên trong công ty có cái nhìn toàn diện về lập trình những mảng kỹ thuật trên thế giới, công ty Rivercrane Việt Nam quyết định chế độ 3 tháng 1 lần đưa nhân viên đi học tập tại Nhật. Các bạn kỹ sư hoàn toàn đều có thể quyết định khả năng phát triển bản thân theo hướng kỹ thuật hoặc theo hướng quản lý.

    CHẾ ĐỘ ĐI DU LỊCH HÀNG NĂM

    Không chỉ đưa đến cho nhân viên những công việc thử thách thể hiện bản thân, công ty Rivercrane Việt Nam muốn nhân viên luôn thích thú khi đến với những chuyến hành trình thú vị hàng năm. Những buổi tiệc Gala Dinner sôi động cùng với những trò chơi Team Building vui nhộn sẽ giúp cho đại gia đình Rivercrane thân thiết hơn.

    CHẾ ĐỘ EVENT CÔNG TY

    Những hoạt động Team building, Company Building, Family Building, Summer Holiday, Mid-Autumn Festival… sẽ là những khoảnh khắc gắn kết đáng nhớ của mỗi một nhân viên trong từng dự án, hoặc sẽ là những điều tự hào khi giới thiệu công ty mình với với gia đình thân thương, cùng nhau chia sẻ yêu thương với thông điệp “We are One”

    BẢO HIỂM

    Công ty Rivercrane Việt Nam đảm bảo tham gia đầy đủ chế độ Bảo hiểm xã hội, bảo hiểm y tế và bảo hiểm thất nghiệp. Cam kết chặt chẽ về mọi thủ tục phát sinh công ty đều hỗ trợ và tiến hành cho nhân viên từ đầu đến cuối. Những chế độ bảo hiểm khác công ty cũng đặc biệt quan tâm và từng bước tiến hành.

    CHẾ ĐỘ PHÚC LỢI KHÁC

    Hỗ trợ kinh phí cho các hoạt động văn hóa, văn nghệ, thể thao; Hỗ trợ kinh phí cho việc mua sách nghiên cứu kỹ thuật; Hỗ trợ kinh phí thi cử bằng cấp kỹ sư, bằng cấp dành cho ngôn ngữ. Hỗ trợ kinh phí tham gia các lớp học về quản lý kỹ thuật bên ngoài; Các hỗ trợ phúc lợi khác theo quy định công ty…

    CÔNG VIỆC TƯƠNG TỰ

    © 2012 RiverCrane Vietnam. All rights reserved.

    Close