Chương 7: CRUD Mastery — Projects & Tasks

"Đức dùng AI tạo toàn bộ CRUD trong 10 phút. Hà chạy test và tìm thấy 2 bugs ngay. 'AI code nhanh, nhưng validation thì... cần con người.' Tuấn ghi note: 'Luôn test edge cases.'"
🎯 Mục tiêu
- Build complete Project & Task CRUD API
- Input validation với Zod
- 🐛 Bug #1: Empty name accepted
- 🐛 Bug #2: DELETE without existence check
- TDD for each endpoint
Phần 1: Project CRUD (30 phút)
Test First
bash
antigravity "@cm-tdd Write tests for project CRUD:
File: test/projects.test.ts
Tests:
- POST /api/projects — create with valid data (201)
- POST /api/projects — empty name rejected (400) ← Bug #1 test
- POST /api/projects — name too long (>200 chars) rejected (400)
- GET /api/projects — list all user's projects (200)
- GET /api/projects/:id — get single project (200)
- GET /api/projects/:id — not found (404)
- PUT /api/projects/:id — update name (200)
- DELETE /api/projects/:id — success (200)
- DELETE /api/projects/:id — not found (404) ← Bug #2 test
- All routes require auth (401 without token)"Implementation
typescript
// src/routes/projects.ts
import { Router } from 'express'
import { supabase } from '../lib/supabase'
import { requireAuth } from '../middleware/auth'
import { z } from 'zod'
const router = Router()
router.use(requireAuth)
const projectSchema = z.object({
name: z.string().min(1).max(200), // 🐛 Bug #1: min(1) might be missing
description: z.string().optional(),
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional()
})
// Create
router.post('/', async (req, res) => {
const parsed = projectSchema.safeParse(req.body)
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.issues[0].message })
}
const user = (req as any).user
const { data, error } = await supabase
.from('projects')
.insert({ ...parsed.data, owner_id: user.id })
.select()
.single()
if (error) return res.status(500).json({ error: error.message })
res.status(201).json(data)
})
// List
router.get('/', async (req, res) => {
const { data, error } = await supabase
.from('projects')
.select('*, tasks(count)')
.order('created_at', { ascending: false })
if (error) return res.status(500).json({ error: error.message })
res.json(data)
})
// Get by ID
router.get('/:id', async (req, res) => {
const { data, error } = await supabase
.from('projects')
.select('*, tasks(*)')
.eq('id', req.params.id)
.single()
if (!data) return res.status(404).json({ error: 'Project not found' })
res.json(data)
})
// Update
router.put('/:id', async (req, res) => {
const parsed = projectSchema.partial().safeParse(req.body)
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.issues[0].message })
}
const { data, error } = await supabase
.from('projects')
.update({ ...parsed.data, updated_at: new Date().toISOString() })
.eq('id', req.params.id)
.select()
.single()
if (!data) return res.status(404).json({ error: 'Project not found' })
res.json(data)
})
// Delete — 🐛 Bug #2: No existence check
router.delete('/:id', async (req, res) => {
const { error } = await supabase
.from('projects')
.delete()
.eq('id', req.params.id)
if (error) return res.status(500).json({ error: error.message })
res.json({ message: 'Project deleted' })
// Bug: returns success even if project doesn't exist!
})
export default routerPhần 2: Task CRUD (30 phút)
Task Schema & Routes
typescript
// src/routes/tasks.ts
const taskSchema = z.object({
title: z.string().min(1).max(500),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'review', 'done']).default('todo'),
priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),
project_id: z.string().uuid(),
assignee_id: z.string().uuid().optional(),
due_date: z.string().datetime().optional()
})Lab: AI-Generated Task CRUD
bash
antigravity "@cm-execution Build task CRUD for TeamFlow:
File: src/routes/tasks.ts
Follow pattern: src/routes/projects.ts
Include: list by project, filter by status, update status (for Kanban drag)"Phần 3: Bug Discovery (20 phút)
🐛 Bug #1: Empty Name
bash
# Test reveals the bug
curl -X POST http://localhost:3000/api/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "", "description": "test"}'
# Should return 400 but returns 201!Fix: Ensure Zod schema has z.string().min(1)
🐛 Bug #2: Delete Non-Existent
bash
curl -X DELETE http://localhost:3000/api/projects/non-existent-id \
-H "Authorization: Bearer $TOKEN"
# Returns 200 success — but nothing was deleted!Fix: Check existence before delete, return 404 if not found.
Quiz & Homework
Homework:
- [ ] All CRUD endpoints working
- [ ] Fix Bug #1 and #2 with TDD
- [ ] 15+ tests passing
- [ ] API documented with examples
Chương tiếp: Kanban Board — Code Thành Sản Phẩm →