1. Giới thiệu dự án thực chiến
Trong 3 bài tiếp theo, chúng ta sẽ xây dựng Task Management App hoàn chỉnh bằng Vibe Coding. Mục tiêu là trải nghiệm quy trình phát triển end-to-end với AI.
Tổng quan dự án:
| Layer | Technology | Bài học |
|---|---|---|
| Backend | Node.js + Express + TypeScript | Bài 14 (bài này) |
| Frontend | React + Next.js + TailwindCSS | Bài 15 |
| Mobile | React Native / Flutter | Bài 16 |
| Database | PostgreSQL + Prisma ORM | Bài 14 |
| Auth | JWT + bcrypt | Bài 14 |
2. Khởi tạo project bằng Vibe Coding
Bước 1: Dùng Agent Mode để scaffold
// Prompt cho Agent Mode:
Create a new Node.js backend project with:
- TypeScript configuration
- Express.js server
- Prisma ORM with PostgreSQL
- JWT authentication
- Project structure following clean architecture
- ESLint + Prettier configuration
- Docker Compose for PostgreSQL
Agent Mode sẽ tự động:
- Chạy
npm init, cài dependencies - Tạo
tsconfig.json,.eslintrc,.prettierrc - Tạo cấu trúc thư mục
- Setup Docker Compose
Bước 2: Project structure được tạo ra
task-api/
├── src/
│ ├── controllers/
│ │ ├── auth.controller.ts
│ │ └── task.controller.ts
│ ├── middleware/
│ │ ├── auth.middleware.ts
│ │ └── validate.middleware.ts
│ ├── routes/
│ │ ├── auth.routes.ts
│ │ └── task.routes.ts
│ ├── services/
│ │ ├── auth.service.ts
│ │ └── task.service.ts
│ ├── types/
│ │ └── index.ts
│ └── app.ts
├── prisma/
│ └── schema.prisma
├── docker-compose.yml
├── tsconfig.json
└── package.json
3. Database Schema Design với AI
// Prompt:
Design a Prisma schema for a task management app with:
- Users (email, password hash, name, avatar)
- Projects (name, description, owner)
- Tasks (title, description, status, priority, assignee, due date)
- Comments on tasks
- Project memberships with roles
Copilot sẽ generate schema:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
name String
avatar String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ownedProjects Project[]
memberships ProjectMember[]
assignedTasks Task[] @relation("AssignedTasks")
createdTasks Task[] @relation("CreatedTasks")
comments Comment[]
}
model Project {
id String @id @default(uuid())
name String
description String?
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id])
members ProjectMember[]
tasks Task[]
}
model ProjectMember {
id String @id @default(uuid())
role Role @default(MEMBER)
userId String
projectId String
user User @relation(fields: [userId], references: [id])
project Project @relation(fields: [projectId], references: [id])
@@unique([userId, projectId])
}
enum Role {
OWNER
ADMIN
MEMBER
}
model Task {
id String @id @default(uuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
dueDate DateTime?
projectId String
assigneeId String?
creatorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id])
assignee User? @relation("AssignedTasks", fields: [assigneeId], references: [id])
creator User @relation("CreatedTasks", fields: [creatorId], references: [id])
comments Comment[]
}
enum TaskStatus {
TODO
IN_PROGRESS
IN_REVIEW
DONE
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}
model Comment {
id String @id @default(uuid())
content String
taskId String
authorId String
createdAt DateTime @default(now())
task Task @relation(fields: [taskId], references: [id])
author User @relation(fields: [authorId], references: [id])
}
4. Authentication — JWT Flow
// Prompt:
Implement JWT authentication with:
- Register endpoint with email validation and password hashing
- Login endpoint returning access + refresh tokens
- Auth middleware that validates JWT
- Refresh token rotation
- Use bcrypt for password hashing
Copilot generate auth service:
// src/services/auth.service.ts
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../lib/prisma';
export class AuthService {
async register(email: string, password: string, name: string) {
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
throw new Error('Email already registered');
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: { email, password: hashedPassword, name },
});
return this.generateTokens(user.id);
}
async login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new Error('Invalid credentials');
}
return this.generateTokens(user.id);
}
private generateTokens(userId: string) {
const accessToken = jwt.sign(
{ userId },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
}
5. REST API — CRUD Operations
// Prompt:
Create CRUD endpoints for tasks with:
- GET /api/tasks — list tasks with filtering and pagination
- POST /api/tasks — create new task
- PUT /api/tasks/:id — update task
- DELETE /api/tasks/:id — delete task
- Include authorization checks
- Validate request body with Zod
Copilot generate controller:
// src/controllers/task.controller.ts
import { Request, Response } from 'express';
import { z } from 'zod';
import { prisma } from '../lib/prisma';
const createTaskSchema = z.object({
title: z.string().min(1).max(255),
description: z.string().optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).default('MEDIUM'),
dueDate: z.string().datetime().optional(),
projectId: z.string().uuid(),
assigneeId: z.string().uuid().optional(),
});
export class TaskController {
async list(req: Request, res: Response) {
const { projectId, status, priority, page = '1', limit = '20' } = req.query;
const tasks = await prisma.task.findMany({
where: {
projectId: projectId as string,
status: status as any,
priority: priority as any,
},
include: {
assignee: { select: { id: true, name: true, avatar: true } },
_count: { select: { comments: true } },
},
skip: (Number(page) - 1) * Number(limit),
take: Number(limit),
orderBy: { createdAt: 'desc' },
});
res.json({ data: tasks });
}
async create(req: Request, res: Response) {
const data = createTaskSchema.parse(req.body);
const task = await prisma.task.create({
data: {
...data,
dueDate: data.dueDate ? new Date(data.dueDate) : null,
creatorId: req.userId!, // from auth middleware
},
});
res.status(201).json({ data: task });
}
}
6. Vibe Coding Workflow cho Backend
Quy trình tối ưu:
┌──────────────────────────────────────────────────┐
│ BACKEND VIBE CODING WORKFLOW │
├──────────────────────────────────────────────────┤
│ │
│ 1. Mô tả feature bằng ngôn ngữ tự nhiên │
│ ↓ │
│ 2. Agent Mode scaffold files │
│ ↓ │
│ 3. Review generated code │
│ ↓ │
│ 4. Prompt: "Write tests for this" │
│ ↓ │
│ 5. Agent chạy tests, fix lỗi │
│ ↓ │
│ 6. Prompt: "Add error handling and validation" │
│ ↓ │
│ 7. Review final code → Commit │
│ │
└──────────────────────────────────────────────────┘
Tips khi Vibe Coding backend:
- Mô tả rõ business logic: AI không biết domain của bạn
- Yêu cầu tests cùng lúc: "Create the service AND its unit tests"
- Review security: Luôn kiểm tra input validation, auth checks
- Database migrations: Dùng
prisma migrate devsau khi AI thay đổi schema - Sử dụng copilot-instructions.md: Định nghĩa coding standards cho project
7. Testing với AI
// Prompt:
Write comprehensive tests for AuthService including:
- Successful registration
- Duplicate email registration
- Successful login
- Invalid credentials
- Token generation and verification
Use Jest with supertest for integration tests.
// src/services/__tests__/auth.service.test.ts
import { AuthService } from '../auth.service';
import { prisma } from '../../lib/prisma';
import bcrypt from 'bcrypt';
describe('AuthService', () => {
const authService = new AuthService();
beforeEach(async () => {
await prisma.user.deleteMany();
});
describe('register', () => {
it('should create user and return tokens', async () => {
const result = await authService.register(
'[email protected]', 'Password123!', 'Test User'
);
expect(result.accessToken).toBeDefined();
expect(result.refreshToken).toBeDefined();
const user = await prisma.user.findUnique({
where: { email: '[email protected]' },
});
expect(user).toBeDefined();
expect(user!.name).toBe('Test User');
});
it('should hash password correctly', async () => {
await authService.register(
'[email protected]', 'Password123!', 'Test User'
);
const user = await prisma.user.findUnique({
where: { email: '[email protected]' },
});
expect(user!.password).not.toBe('Password123!');
expect(await bcrypt.compare('Password123!', user!.password)).toBe(true);
});
it('should reject duplicate email', async () => {
await authService.register(
'[email protected]', 'Password123!', 'Test User'
);
await expect(
authService.register('[email protected]', 'Other456!', 'Other User')
).rejects.toThrow('Email already registered');
});
});
});
8. Error Handling Pattern
// Prompt:
Add global error handling middleware with:
- Custom AppError class with status codes
- Async error wrapper for controllers
- Prisma error handling (unique constraint, not found)
- Request validation errors (Zod)
- Structured error response format
9. Tổng kết
| Bước | Vibe Coding Approach | Thời gian tiết kiệm |
|---|---|---|
| Project setup | Agent Mode scaffold | ~2 giờ → 5 phút |
| DB Schema | Mô tả entities → Prisma schema | ~1 giờ → 10 phút |
| Auth | Describe flow → full implementation | ~4 giờ → 20 phút |
| CRUD API | Describe endpoints → controller + service | ~3 giờ → 15 phút |
| Tests | "Write tests for X" → comprehensive suite | ~3 giờ → 10 phút |
Lưu ý quan trọng: Thời gian tiết kiệm ở trên là lý tưởng. Thực tế bạn vẫn cần review, debug, và tinh chỉnh code AI sinh ra. Vibe Coding giảm thời gian viết code nhưng không giảm thời gian review.
Bài tiếp theo: Xây dựng Frontend cho app — React + Next.js + TailwindCSS, kết nối API, responsive design.