Skip to content

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 router

Phầ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 →

Powered by CodyMaster × VitePress