Một ticket nghe đơn giản: “thêm filter trạng thái vào trang Orders để support tìm đơn refund nhanh hơn”. Nếu làm tay, tôi mở component, thêm dropdown, gọi API với query param, test vài case. Nếu dùng AI coding không có kỷ luật, assistant cũng làm gần như vậy, nhưng có thể sai endpoint, tự thêm mock data, hoặc đổi luôn layout vì thấy “có thể cải thiện UX”. Case study này là cách tôi muốn ship một feature end-to-end với AI: nhanh, nhưng không buông tay lái.
Feature giả định là SaaS admin dashboard, nhưng pattern áp dụng cho nhiều app: có frontend, API đã tồn tại, staging data, preview URL, deploy production. Mục tiêu không phải khoe prompt hay tool. Mục tiêu là workflow.
Ticket ban đầu
Ticket gốc:
Support cần lọc Orders theo trạng thái:
- All
- Pending
- Paid
- Refunded
- Failed
Default là All.
Filter phải sync với URL query để support copy link cho nhau.
Không thay đổi API contract nếu endpoint hiện tại đã hỗ trợ status.
Điểm quan trọng nằm ở câu cuối. Khi dùng AI coding, nếu không nói rõ “endpoint đã tồn tại, reuse”, assistant có thể tạo API mới, mock hook mới, hoặc sửa backend không cần thiết. Trong repo thật, user cũng có rule tương tự: endpoint và hook đã có thì reuse, không assume. Rule này làm scope nhỏ lại và giảm risk.
Yêu cầu AI đọc trước khi sửa
Prompt đầu tiên không phải “implement”. Prompt đầu tiên là “orient”.
Read the Orders page and related hooks.
Find existing API support for status filter.
Do not edit files yet.
Return:
- files involved
- existing query params
- current URL sync behavior
- unknowns
Tôi muốn AI chứng minh nó biết codebase trước. Nếu nó trả lời “có thể dùng /api/orders?status=” mà chưa chỉ ra file hook, tôi chưa cho sửa. Nếu nó nói không tìm thấy hook, tôi kiểm tra lại bằng rg hoặc tự đọc. AI tốt vẫn có thể miss vì codebase nhiều alias, generated clients, feature folders.
Kết quả orientation lý tưởng:
Files:
- src/features/orders/OrdersPage.tsx
- src/features/orders/hooks/useOrders.ts
- src/features/orders/components/OrderFilters.tsx
Existing API:
- useOrders accepts { page, search, status }
- status values are order_status enum: pending, paid, refunded, failed
URL:
- page and search already sync to query params
- status not wired yet
Đến đây mới bắt đầu implementation.
Scope implementation thật nhỏ
Tôi chia feature thành ba thay đổi:
- UI control trong
OrderFilters. - State/query param wiring trong
OrdersPage. - Hook call truyền
status.
Không đổi table. Không đổi API client. Không đổi enum nếu enum đã có. Không chỉnh CSS toàn trang. Không “improve empty state” nếu ticket không yêu cầu.
Prompt implementation:
Implement status filter only.
Use existing useOrders status parameter.
Sync status to URL query param named status.
Default All means no status param.
Do not create mock data.
Do not modify unrelated layout.
Remove debug logs.
Prompt này nghe hơi cứng, nhưng cần thiết. AI coding hay over-helpful. Nó thấy dropdown xấu, tự refactor. Nó thấy duplicate query logic, tự tạo helper. Có lúc đúng, nhưng trong feature nhỏ trước deploy, over-helpful là risk.
Review diff như reviewer khó tính
Sau khi AI sửa, tôi không đọc summary trước. Tôi đọc diff. Summary của AI thường làm mình mềm lòng. Diff mới là sự thật.
Checklist review:
- File touched có đúng scope không.
- Có mock data không.
- Có endpoint mới không.
- Query param default có đúng không.
- Status value gửi lên API có đúng enum không.
- URL copy/paste có restore filter không.
- Có
console.loghoặc debug UI không. - Import order và unused imports có sạch không.
Một lỗi tôi gặp nhiều: All được gửi thành status=all. Backend không hiểu, trả empty list. UI nhìn như không có đơn hàng. Đúng behavior là All không gửi param.
Một lỗi khác: AI sync URL bằng useEffect nhưng đưa cả function hoặc translation function vào dependency làm effect chạy lại không cần thiết. Trong repo React có rule “không add t vào dependencies” cũng vì các case như vậy. Đây là ví dụ vì sao local rules quan trọng hơn best practice chung chung.
Test bằng data có ý nghĩa
Test feature filter không thể dùng data ngẫu nhiên. Cần biết staging có order ở từng trạng thái. Nếu staging thiếu refunded, tạo fixture qua flow thật hoặc nhờ backend seed, nhưng đừng fake trong frontend.
Smoke local hoặc preview:
Orders status filter smoke
- Open /orders, confirm no status query param.
- Select Pending, URL becomes /orders?status=pending.
- Refresh, Pending remains selected.
- Copy URL to new tab, Pending remains selected.
- Select All, status param is removed.
- Select Refunded, table shows known refunded order ORD-1009.
- Search keyword while Refunded selected, both filters apply.
Điểm “known refunded order ORD-1009” rất quan trọng. Nếu chỉ nhìn table có row, bạn không biết filter đúng hay data tình cờ trùng. Field guide thực tế luôn cần một record làm mốc.
Dùng AI viết test, nhưng tự chọn case
Nếu project đã có test setup, tôi để AI thêm test gần behavior. Nhưng tôi không bảo “add tests” chung chung. Tôi đưa case:
Add focused tests for:
- default All does not set status param
- selecting Paid sets status=paid and calls useOrders with paid
- loading URL ?status=refunded selects Refunded
- selecting All removes status param
AI khá tốt ở việc viết test theo pattern có sẵn. Điều kiện là nó phải đọc test cũ trước. Nếu codebase dùng React Testing Library, dùng đúng. Nếu dùng Playwright component test, dùng đúng. Nếu chưa có test infra cho page này, đừng để AI tự dựng cả framework trong feature branch. Khi blast radius nhỏ, manual smoke documented có thể tốt hơn một test framework mới tạo vội.
Preview URL và reviewer handoff
PR description nên có phần cho reviewer bấm và kiểm:
Preview:
- URL: https://preview.example.com/orders
Changed behavior:
- Orders page now supports status filter.
- Filter syncs with URL query param `status`.
- All removes the param.
Smoke:
- Tested `?status=refunded` with ORD-1009.
- Tested copy/paste URL in new tab.
- Tested search + status together.
Đây là nơi AI giúp tốt. Nó có thể lấy diff và tạo PR summary. Nhưng summary phải chứa bằng chứng thật từ smoke test, không phải câu “tested locally”. Nếu bạn chưa test ORD-1009, đừng để AI viết rằng đã test.
Reviewer không cần đọc toàn bộ suy nghĩ của bạn. Reviewer cần biết behavior đổi ở đâu, test gì rồi, rủi ro còn gì.
Deploy plan ngắn
Feature này medium risk: UI + query param + API call, không migration. Deploy plan:
Deploy plan
- Deploy window: business hours, owner online 15 min.
- Rollback point: previous frontend deploy ID.
- Post-deploy smoke:
- open /orders
- select Refunded
- confirm known refunded order appears
- refresh URL
- select All and confirm param removed
Không cần ceremony quá lớn. Nhưng cần rollback point và post-deploy smoke. Nếu sau deploy support báo Orders empty, việc đầu tiên là mở URL với status param, check network request, rồi rollback nếu symptom rõ từ deploy mới.
Chỗ AI hay làm sai trong case này
Nhìn feature nhỏ, nhưng có nhiều bẫy:
- Tạo status enum mới: trong khi backend/generated client đã có enum.
- Gửi
all: thay vì omit param. - Không encode URL đúng: query param bị mất khi combine với search/page.
- Reset pagination sai: đổi status nhưng vẫn ở page 7, user thấy empty.
- Không preserve search: chọn status làm mất keyword search.
- Refactor component: đổi layout nhiều hơn ticket.
- Dùng mock data: làm preview không chứng minh API contract.
- Không test refresh: state nhìn đúng cho đến khi reload.
AI có thể tránh nếu prompt rõ, nhưng reviewer vẫn phải bắt. Đừng confuse “AI đã làm đúng 80%” với “feature đã sẵn sàng”.
Handoff cho support
Một feature cho support cần một note support dùng được. Không phải docs dài. Chỉ cần:
Orders filter shipped:
- Support can filter by Pending, Paid, Refunded, Failed.
- Filter is shareable via URL, e.g. /orders?status=refunded.
- All clears the status filter.
- Existing search can combine with status.
Đây cũng là cách kiểm tra feature có rõ không. Nếu bạn không giải thích cho support trong bốn dòng, feature có thể đang quá mơ hồ.
Pattern tổng quát
Case Orders này không đặc biệt. Pattern là:
- Orient trước khi edit.
- Reuse existing API/hook.
- Scope nhỏ.
- Review diff theo risk.
- Smoke bằng data thật.
- Preview URL cho reviewer.
- Deploy với rollback point.
- Handoff ngắn cho người dùng nội bộ.
AI coding mạnh nhất khi nó tăng tốc từng bước trong pattern này. Nó đọc code, sửa boilerplate, viết test, tóm tắt PR, đề xuất smoke. Nhưng con người vẫn giữ quyền định nghĩa done.
Done không phải “AI nói đã implement”. Done là preview chạy, behavior đúng với data thật, diff đúng scope, deploy có rollback, và người dùng biết cách dùng. Nếu thiếu một trong các điểm đó, feature chưa end-to-end. Nó mới chỉ là code.