Yêu cầu và thiết kế
| Tính năng | Công nghệ | Role |
|---|---|---|
| Đăng ký / đăng nhập | JWT + bcrypt | Ai cũng dùng |
| Xem danh sách việc | GET /api/jobs | Ai cũng dùng |
| Tạo / sửa / xoá việc | POST/PUT/DELETE | Chỉ recruiter |
| Ứng tuyển | POST /api/applications | Chỉ ứng viên |
| Cache danh sách | Redis TTL | Backend |
Thiết kế database
// File: schema.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL, -- bcrypt hash
role TEXT NOT NULL DEFAULT 'candidate' CHECK (role IN ('candidate','recruiter')),
name TEXT NOT NULL
);
CREATE TABLE jobs (
id SERIAL PRIMARY KEY,
recruiter_id INT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
description TEXT NOT NULL,
salary_min INT, salary_max INT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE applications (
id SERIAL PRIMARY KEY,
job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
user_id INT NOT NULL REFERENCES users(id),
note TEXT,
UNIQUE (job_id, user_id) -- một người ứng một việc một lần
);
Xác thực và phân quyền
// File: auth.middleware.js
// requireAuth: gắn req.user từ JWT
// requireRole("recruiter"): kiểm tra req.user.role
function requireRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: "Không có quyền" });
}
next();
};
}
app.post("/api/jobs", requireAuth, requireRole("recruiter"), async (req, res) => {
const job = await db.job.create({
data: { ...req.body, recruiter_id: req.user.userId },
});
res.status(201).json(job);
});
Thêm Redis cache
// File: cache.js
// Cache danh sách việc 60 giây để giảm tải DB
app.get("/api/jobs", async (req, res) => {
const cacheKey = "jobs:list:v1";
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
const jobs = await db.job.findMany({ orderBy: { created_at: "desc" } });
await redis.set(cacheKey, JSON.stringify(jobs), { EX: 60 });
res.json(jobs);
});
// Khi tạo/sửa/xoá việc -> xoá cache để không phục vụ dữ liệu cũ
await redis.del("jobs:list:v1");
Chạy thử và checklist hoàn thiện
// File: test.sh
# Khởi động
docker compose up -d db redis # PostgreSQL + Redis
npm run migrate # tạo bảng
npm run dev # backend :3000
npm run dev --prefix frontend # frontend :5173
# Thử luồng chính
curl -X POST localhost:3000/api/auth/register -d '{"email":"a@b.c","password":"123456"}'
curl -X POST localhost:3000/api/auth/login -d '{"email":"a@b.c","password":"123456"}'
curl localhost:3000/api/jobs -H "Authorization: Bearer <TOKEN>"
💡 Để dự án nổi bật trong CV
Thêm vài thứ nhỏ nhưng đắt giá: validate bằng zod, xử lý lỗi tập trung, README có ảnh demo, deploy link (để dành khoá Engineer). Chất lượng giấy tờ thường quyết định 50% ấn tượng.
❓ Một ứng viên (candidate) cố tình gửi DELETE /api/jobs/1. Server nên trả về gì?
- Thiết kế schema đạt 3NF với bảng trung gian
- JWT + bcrypt + phân quyền theo role
- Redis cache danh sách việc
- README + screenshot cho dự án CV