Bài 7. Kết nối MySQL và xây dựng REST API

Bài này nối Express với MySQL (dùng lại CSDL mẫu qlsv của loạt SQL) qua thư viện mysql2, viết đủ API CRUD sinh viên theo chuẩn REST, kiểm tra dữ liệu bằng Zod và trả lỗi thống nhất. Kết quả là một backend thật cho trang web hoặc ứng dụng Flutter.

60 phútTrung cấp
Bạn sẽ học được
  • Kết nối MySQL bằng mysql2/promise và connection pool
  • Truy vấn an toàn bằng placeholder ?
  • Viết API REST: GET, POST, PUT/PATCH, DELETE
  • Kiểm tra dữ liệu bằng Zod, phân trang, mã trạng thái đúng

Một số khái niệm

Thiết kế tài nguyên giống loạt PHP Bài 18: GET /api/sinhvien, GET /api/sinhvien/:id, POST, PATCH, DELETE. Dùng connection pool để tái sử dụng kết nối giữa các yêu cầu. Luôn dùng placeholder ? (mysql2 tự dùng prepared statement với execute) để chống SQL Injection.

Kết nối CSDL

npm install mysql2 zod
// src/db.js
import mysql from 'mysql2/promise';
export const pool = mysql.createPool({
  host: process.env.DB_HOST, user: process.env.DB_USER,
  password: process.env.DB_PASS, database: process.env.DB_NAME,
  connectionLimit: 10, dateStrings: true,
});

API CRUD

// src/routes/sinhvien.js
import { Router } from 'express';
import { z } from 'zod';
import { pool } from '../db.js';
const r = Router();

const SinhVien = z.object({
  ma_sv: z.string().regex(/^SV\d{3,8}$/, 'Mã dạng SV + số'),
  ho_ten: z.string().trim().min(3),
  email: z.email().optional(),
  lop_id: z.coerce.number().int().positive(),
});

r.get('/', async (req, res) => {
  const q = `%${req.query.q ?? ''}%`;
  const trang = Math.max(1, Number(req.query.page) || 1), soDong = 10;
  const [rows] = await pool.execute(
    'SELECT id, ma_sv, ho_ten, email, lop_id FROM sinhvien WHERE ho_ten LIKE ? ORDER BY ho_ten LIMIT ? OFFSET ?',
    [q, String(soDong), String((trang - 1) * soDong)]);
  const [[{ tong }]] = await pool.execute('SELECT COUNT(*) AS tong FROM sinhvien WHERE ho_ten LIKE ?', [q]);
  res.json({ du_lieu: rows, trang, tong });
});

r.get('/:id', async (req, res) => {
  const [rows] = await pool.execute('SELECT * FROM sinhvien WHERE id = ?', [req.params.id]);
  if (!rows.length) return res.status(404).json({ loi: 'Không tìm thấy sinh viên' });
  res.json(rows[0]);
});

r.post('/', async (req, res) => {
  const kq = SinhVien.safeParse(req.body);
  if (!kq.success) return res.status(422).json({ loi: 'Dữ liệu không hợp lệ', chi_tiet: z.flattenError(kq.error).fieldErrors });
  const { ma_sv, ho_ten, email = null, lop_id } = kq.data;
  try {
    const [r1] = await pool.execute('INSERT INTO sinhvien (ma_sv, ho_ten, email, lop_id) VALUES (?, ?, ?, ?)', [ma_sv, ho_ten, email, lop_id]);
    res.status(201).json({ id: r1.insertId, ...kq.data });
  } catch (e) {
    if (e.code === 'ER_DUP_ENTRY') return res.status(409).json({ loi: 'Mã sinh viên đã tồn tại' });
    throw e;
  }
});

r.patch('/:id', async (req, res) => {
  const kq = SinhVien.partial().safeParse(req.body);
  if (!kq.success) return res.status(422).json({ loi: 'Dữ liệu không hợp lệ' });
  const cot = Object.keys(kq.data);
  if (!cot.length) return res.status(400).json({ loi: 'Không có trường cần sửa' });
  const [r1] = await pool.execute(`UPDATE sinhvien SET ${cot.map(c => c + ' = ?').join(', ')} WHERE id = ?`, [...Object.values(kq.data), req.params.id]);
  if (!r1.affectedRows) return res.status(404).json({ loi: 'Không tìm thấy' });
  res.json({ ok: true });
});

r.delete('/:id', async (req, res) => {
  const [r1] = await pool.execute('DELETE FROM sinhvien WHERE id = ?', [req.params.id]);
  r1.affectedRows ? res.status(204).end() : res.status(404).json({ loi: 'Không tìm thấy' });
});
export default r;

Tên cột trong câu PATCH lấy từ khóa của schema Zod (danh sách trắng), nên an toàn dù ghép vào chuỗi SQL.

ORM: Prisma và Drizzle

Với dự án lớn, có thể dùng ORM để làm việc với CSDL bằng đối tượng và có kiểu TypeScript: Prisma (schema riêng, công cụ migrate mạnh) hoặc Drizzle (gần SQL, nhẹ). Cả hai hỗ trợ MySQL, PostgreSQL, SQLite và dùng tốt với Next.js.

Mẹo hay

Viết tệp api.http cho tiện ích REST Client của VS Code chứa sẵn các yêu cầu thử API, commit cùng mã nguồn để đồng đội dùng.

Lưu ý

Không trả lỗi CSDL thô (e.message) cho client vì lộ cấu trúc bảng; ghi log nội bộ và trả thông báo chung.

Thực hành
  1. Kết nối CSDL qlsv, viết GET danh sách có tìm kiếm, phân trang.
  2. Hoàn thiện POST, PATCH, DELETE với Zod.
  3. Thêm API /api/lop và /api/sinhvien/:id/diem (JOIN).
  4. Gọi API từ trang JavaScript hoặc ứng dụng Flutter.
Tóm tắt bài học
  • mysql2/promise + pool; execute với placeholder ?.
  • REST: tài nguyên + phương thức + mã trạng thái.
  • Zod kiểm tra dữ liệu, trả 422 kèm chi tiết.
  • ORM Prisma/Drizzle cho dự án lớn.