DDevArchive
Đăng nhập

Dự án Giai đoạn 2: ứng dụng CRUD hoàn chỉnh

Tổng hợp toàn bộ: một app quản lý việc làm (job board) nhỏ có đăng ký/đăng nhập bằng JWT, CRUD công việc theo role, dữ liệu PostgreSQL, cache danh sách bằng Redis. Đây là dự án bạn sẽ đưa vào CV.

Yêu cầu và thiết kế

Tính năngCông nghệRole
Đăng ký / đăng nhậpJWT + bcryptAi cũng dùng
Xem danh sách việcGET /api/jobsAi cũng dùng
Tạo / sửa / xoá việcPOST/PUT/DELETEChỉ recruiter
Ứng tuyểnPOST /api/applicationsChỉ ứng viên
Cache danh sáchRedis TTLBackend

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