Tìm hiểu cách tạo spec tính năng từ mã với Claude Code bằng cách trích hành vi thực từ routes và components, rồi tạo spec sống và danh sách gaps.

Mọi người tranh luận về app vì họ nhớ những phiên bản khác nhau. Support nhớ ticket gần nhất. Sales nhớ đường dẫn demo. Kỹ sư nhớ tính năng như nó được thiết kế. Hỏi ba người và bạn có ba câu trả lời chắc chắn, nhưng không ai trùng với bản build hiện tại.
Theo thời gian, mã trở thành nguồn duy nhất luôn cập nhật. Tài liệu trôi dạt, ticket đóng, và các sửa nhanh chồng chất. Một route có quy tắc xác thực mới. Một toggle UI thay đổi mặc định. Một handler bắt đầu trả các lỗi khác. Không ai cập nhật spec vì nó cảm thấy là tùy chọn, và mỗi thay đổi đều quá nhỏ để ghi nhận.
Điều đó tạo ra những vấn đề dễ đoán. Team phát hành thay đổi làm vỡ các trường hợp cạnh mà họ không biết tồn tại. QA test con đường thuận lợi và bỏ sót quy tắc nằm sâu trong handler. Đồng nghiệp mới sao chép hành vi từ UI mà không hiểu ràng buộc thực sự. Stakeholder tranh luận ý kiến thay vì chỉ vào hành vi đã thỏa thuận.
Kết quả tốt không phải là một tài liệu hoàn hảo. Là sự rõ ràng chung. Ai cũng nên trả lời được: "Chuyện gì xảy ra nếu tôi làm X?" và "Hệ thống đảm bảo điều gì?" mà không phải đoán. Bạn sẽ có ít bất ngờ hơn, vòng review ngắn hơn và ít khoảnh khắc "Khoan, nó đã làm được rồi" vì cả đội đang nhìn cùng một sự thật.
Khi spec khớp với mã, việc lên kế hoạch thay đổi trở nên an toàn. Bạn có thể nhận ra thứ nào ổn định, thứ nào là tai nạn, và thứ gì còn thiếu trước khi phát hành.
Spec sống là một mô tả ngắn, có thể chỉnh sửa về những gì app thực sự làm hôm nay. Nó không phải tài liệu một lần. Nó thay đổi mỗi khi hành vi thay đổi, nên team có thể tin tưởng.
Khi người ta nói về spec tính năng viết từ mã (ví dụ, dùng Claude Code), mục tiêu đơn giản: đọc hành vi thực từ routes, handlers và màn hình, rồi viết lại bằng ngôn ngữ rõ ràng.
Một spec sống hữu ích tập trung vào những gì người dùng nhìn thấy và những gì hệ thống cam kết. Nó nên bao gồm:
Những điều không nên ghi là cách mã được tổ chức. Nếu bạn bắt đầu đặt tên file và kế hoạch refactor, bạn đang sa vào chi tiết triển khai. Tránh:
Danh sách gaps là riêng biệt. Đó là một danh sách nhỏ các bất đồng và những điều không rõ bạn tìm thấy khi viết spec.
Ví dụ: một route từ chối file lớn hơn 10MB nhưng UI nói 25MB. Đó là một gap cho đến khi team quyết định quy tắc nào là thực và cập nhật code hoặc spec.
Bắt đầu nhỏ. Nếu bạn cố gắng tài liệu hóa cả app, bạn sẽ kết thúc với một đống ghi chú không ai tin tưởng. Chọn một lát nhỏ mà người dùng có thể mô tả bằng một câu, như "mời đồng đội", "thanh toán", hoặc "đặt lại mật khẩu." Phạm vi tốt là một vùng tính năng đơn, một module, hoặc một hành trình người dùng từ điểm vào đến kết quả.
Chọn điểm vào dựa trên nơi sự thật tồn tại:
Trước khi đọc mã, thu thập vài đầu vào để sai lệch nổi bật nhanh: tài liệu API hiện có, ghi chú sản phẩm cũ, ticket support, và các "điểm đau" đã biết mọi người than phiền. Những thứ này không ghi đè mã, nhưng giúp bạn nhận ra trạng thái thiếu như lỗi, case cạnh và quyền truy cập.
Giữ định dạng spec nhàm và nhất quán. Team dễ đồng thuận khi mọi spec đọc giống nhau.
Dùng cấu trúc này lặp lại sẽ giúp spec dễ đọc, dễ so sánh và dễ cập nhật.
Bắt đầu từ điểm vào server. Routes và handlers cho thấy "app làm gì" một cách cụ thể: ai gọi được, họ phải gửi gì, họ nhận lại gì, và hệ thống thay đổi gì.
Liệt kê các route trong phạm vi và ánh xạ mỗi route tới mục đích người dùng. Đừng viết "POST /api/orders." Hãy viết "Đặt hàng" hoặc "Lưu nháp." Nếu bạn không thể đặt tên mục đích bằng ngôn ngữ đơn giản, đó đã là một gap spec.
Khi đọc từng handler, ghi các input và quy tắc xác thực thành yêu cầu hướng người dùng. Bao gồm trường bắt buộc, định dạng cho phép, và các quy tắc gây lỗi thực sự. Ví dụ: "Email phải hợp lệ", "Số lượng phải ít nhất 1", "Ngày bắt đầu không thể ở quá khứ."
Viết kiểm tra auth và role theo cách tương tự. Thay vì "middleware: requireAdmin," ghi: "Chỉ admins mới có thể hủy mọi đơn. Người dùng thường chỉ hủy đơn của chính họ trong vòng 10 phút." Nếu mã kiểm tra ownership, feature flag, hoặc ranh giới tenant, cũng đưa vào.
Rồi ghi các output và kết quả. Thành công trả gì (ID được tạo, object cập nhật)? Các lỗi phổ biến trông ra sao (401 chưa đăng nhập, 403 không được phép, 404 không tìm thấy, 409 xung đột, 422 lỗi xác thực)?
Cuối cùng, ghi side effects vì chúng là một phần của hành vi: bản ghi được tạo/cập nhật, email/notification gửi, event publish, job nền xếp hàng, và bất cứ điều gì kích hoạt luồng khác. Những chi tiết này ngăn ngừa bất ngờ khi team sau này dựa vào spec.
Routes cho bạn biết app có thể làm gì. Components cho bạn biết người dùng thực sự trải nghiệm ra sao. Xem UI như một phần của hợp đồng: cái gì hiển thị, cái gì bị chặn, và chuyện gì xảy ra khi có lỗi.
Bắt đầu bằng cách tìm các màn hình entry cho tính năng. Tìm component trang, layout wrapper, và vài component "quyết định" kiểm soát fetching, quyền truy cập, và điều hướng. Đó thường là nơi hành vi thực nằm.
Khi đọc component, ghi các quy tắc người dùng cảm nhận được: khi nào hành động bị disable, bước bắt buộc, trường điều kiện, trạng thái loading, và cách lỗi hiện (lỗi trường inline so với toast, auto-retry, nút "thử lại"). Ghi cả hành vi state và cache như dữ liệu cũ hiện trước, cập nhật lạc quan, hoặc timestamp "lần lưu cuối".
Chú ý các luồng ẩn thay đổi im lặng giao diện người dùng. Tìm feature flag, experiment bucket, và cổng chỉ admin. Ghi cả redirect im lặng, như gửi người chưa đăng nhập đến sign-in hoặc gửi người không có quyền đến màn hình upgrade.
Một ví dụ cụ thể: trên màn hình "Thay đổi Email", ghi rằng nút Lưu giữ disable cho đến khi email hợp lệ, spinner hiện trong lúc request, thành công bật banner xác nhận, và lỗi xác thực backend hiển thị dưới input. Nếu mã hiển thị flag như newEmailFlow, ghi cả hai biến thể và khác biệt của chúng.
Viết mỗi luồng UI thành các bước ngắn (người dùng làm gì, UI phản hồi ra sao) và giữ điều kiện cùng lỗi ở ngay cạnh bước ảnh hưởng. Điều này làm spec dễ đọc và giúp phát hiện gap nhanh hơn.
Ghi chú thô từ routes và components hữu ích nhưng khó thảo luận. Viết lại những gì bạn quan sát thành spec mà PM, designer, QA và engineer đều có thể đọc và đồng ý.
Mẫu thực tế là một user story mỗi route hoặc màn hình. Giữ nhỏ và cụ thể. Ví dụ: "Là người dùng đã đăng nhập, tôi có thể đặt lại mật khẩu để lấy lại quyền truy cập." Nếu mã cho thấy hành vi khác theo role (admin vs user), tách thành các story riêng thay vì giấu trong chú thích.
Rồi viết acceptance criteria phản chiếu các đường dẫn mã thực, không phải sản phẩm lý tưởng. Nếu handler trả 401 khi token thiếu, đó là một tiêu chí. Nếu UI disable submit cho đến khi field hợp lệ, đó là một tiêu chí.
Bao gồm quy tắc dữ liệu bằng ngôn ngữ đơn giản, đặc biệt những thứ gây bất ngờ: giới hạn, thứ tự, tính duy nhất, trường bắt buộc. "Username phải duy nhất (kiểm tra khi lưu)" rõ hơn "unique index."
Trường hợp cạnh thường là khác biệt giữa tài liệu đẹp và tài liệu hữu dụng. Ghi rõ trạng thái rỗng, giá trị null, retry, timeout, và người dùng thấy gì khi API gọi thất bại.
Khi gặp điều chưa biết, đánh dấu thay vì đoán:
Những marker này biến thành các câu hỏi nhanh cho team thay vì giả định im lặng.
Danh sách gaps không phải Jira thứ hai. Là một bản ghi ngắn, dựa trên bằng chứng của nơi mã và hành vi mong muốn không khớp, hoặc nơi không ai giải thích rõ "đúng" là gì. Nếu làm tốt, nó trở thành công cụ để đạt đồng thuận, không phải cuộc chiến lập kế hoạch.
Nghiêm khắc về điều nào tính là gap:
Khi log một gap, gồm ba phần để giữ nó cụ thể:
Bằng chứng là thứ giữ danh sách khỏi biến thành ý kiến. Ví dụ: "POST /checkout/apply-coupon chấp nhận coupon hết hạn, nhưng CouponBanner.tsx chặn chúng ở UI. Tác động: doanh thu và nhầm lẫn người dùng. Loại: bug hoặc missing decision (xác nhận quy tắc mong muốn)."
Giữ nó ngắn. Đặt giới hạn cứng, ví dụ 10 mục cho lần đầu. Nếu tìm 40 vấn đề, gom thành các mẫu (không nhất quán validate, kiểm tra quyền, trạng thái rỗng) và giữ vài ví dụ hàng đầu.
Tránh đặt ngày và lịch trong gaps. Nếu cần ai chịu trách nhiệm, ghi nhẹ: ai nên quyết định (product) hoặc ai có thể xác minh (engineering), rồi chuyển việc lập kế hoạch thực sự sang backlog.
Chọn phạm vi nhỏ, lưu lượng cao: checkout với mã khuyến mãi và tùy chọn vận chuyển. Mục tiêu không phải viết lại sản phẩm, chỉ nắm những gì app làm hôm nay.
Bắt đầu với route backend. Thường quy tắc xuất hiện ở đó trước. Bạn có thể thấy route như POST /checkout/apply-promo, GET /checkout/shipping-options, và POST /checkout/confirm.
Từ các handler đó, viết hành vi bằng lời đơn giản:
Rồi kiểm tra component UI. PromoCodeInput có thể chỉ cập nhật tổng khi có phản hồi thành công và lỗi hiển thị inline dưới input. ShippingOptions có thể chọn tự động phương án rẻ nhất khi tải lần đầu và kích hoạt cập nhật bảng giá đầy đủ khi người dùng thay đổi.
Giờ bạn có spec dễ đọc và một danh sách gaps nhỏ. Ví dụ: thông điệp lỗi giữa route mã và UI khác nhau ("Invalid code" vs "Not eligible"), và không ai biết quy tắc làm tròn thuế rõ ràng (theo dòng hay tổng đơn).
Trong hoạch định, team đồng ý về thực tế trước, rồi quyết định thay đổi. Thay vì tranh luận ý kiến, bạn xem hành vi đã được tài liệu, chọn một bất nhất để sửa, và để phần còn lại là "hành vi hiện tại đã biết" cho đến khi đáng để xem lại.
Spec chỉ có ích nếu team đồng ý nó khớp với thực tế. Làm một buổi đọc ngắn với một kỹ sư và một người sản phẩm. Giữ ngắn: 20–30 phút tập trung vào người dùng có thể làm gì và hệ thống phản ứng ra sao.
Trong buổi đọc, biến các phát biểu thành câu hỏi có/không. "Khi user vào route này, chúng ta luôn trả 403 nếu không có session chứ?" "Trạng thái rỗng này có chủ ý không?" Điều này tách hành vi mong muốn ra khỏi hành vi vô ý lọt vào theo thời gian.
Đồng ý về từ ngữ trước khi chỉnh sửa. Dùng từ người dùng thấy trong UI (nhãn nút, tiêu đề trang, thông báo). Thêm tên nội bộ chỉ khi giúp kỹ sư tìm mã (tên route, tên component). Điều này tránh lệch lạc như product nói "Workspace" trong khi spec dùng "Org."
Để giữ nó cập nhật, rõ trách nhiệm và nhịp độ:
Nếu dùng công cụ như Koder.ai, snapshot và rollback giúp so sánh "trước" và "sau" khi bạn cập nhật spec, đặc biệt sau refactor lớn.
Cách nhanh nhất để mất niềm tin vào spec là mô tả sản phẩm bạn muốn, không phải sản phẩm bạn có. Giữ quy tắc cứng: mỗi phát biểu phải có thể chỉ vào mã hoặc màn hình thực tế.
Một bẫy phổ biến là sao chép cấu trúc mã vào tài liệu. Một spec đọc như "Controller -> Service -> Repository" không phải spec, mà là bản đồ thư mục. Viết bằng ngôn ngữ hướng người dùng: gì kích hoạt hành động, người dùng thấy gì, lưu gì, và lỗi trông ra sao.
Quyền và role thường bị bỏ qua đến cuối cùng, rồi mọi thứ vỡ. Thêm quy tắc truy cập sớm, ngay cả khi lộn xộn. Ghi role nào được xem, tạo, sửa, xóa, xuất, hay phê duyệt, và nơi quy tắc được thực thi (chỉ UI, chỉ API, hoặc cả hai).
Đừng bỏ qua non-happy paths. Hành vi thực ẩn trong retry, thất bại từng phần, và quy tắc theo thời gian như hết hạn, cooldown, job theo lịch, hoặc giới hạn "mỗi ngày một lần". Đối xử chúng như hành vi hạng nhất.
Một cách nhanh để lộ gap là kiểm tra:
Cuối cùng, giữ danh sách gaps chuyển động. Mỗi gap nên được dán nhãn: "unknown, needs decision," "bug, fix," hoặc "missing feature, plan." Nếu không có nhãn, danh sách sẽ đình trệ và spec ngừng "sống."
Lướt nhanh để kiểm tra độ rõ ràng, phạm vi và khả năng hành động. Người không viết spec phải hiểu tính năng hoạt động hôm nay và điều gì chưa rõ.
Đọc spec như một đồng đội mới ngày đầu. Nếu họ có thể tóm tắt tính năng trong một phút, bạn gần xong. Nếu họ liên tục hỏi "chỗ này bắt đầu ở đâu?" hoặc "đường chính là gì?" thì mở phần mô tả đầu.
Kiểm tra:
Mỗi gap nên cụ thể và có thể kiểm tra. Thay vì "Xử lý lỗi không rõ", viết: "Nếu nhà cung cấp thanh toán trả 402, UI hiện toast chung; xác nhận thông điệp mong muốn và hành vi retry." Thêm một hành động tiếp theo duy nhất (hỏi product, thêm test, kiểm tra log) và ghi ai trả lời.
Chọn một khu vực tính năng và giới hạn thời gian 60 phút. Chọn thứ gì đó nhỏ nhưng thực (login, checkout, search, một màn hình admin). Viết một câu mô tả phạm vi: bao gồm gì và ngoại trừ gì.
Chạy workflow một lần từ đầu đến cuối: lướt qua các route/handler chính, truy vết luồng UI chính, và ghi lại hành vi quan sát được (input, output, validate, trạng thái lỗi). Nếu bí, log câu hỏi là một gap và tiếp tục.
Khi xong, chia sẻ spec để team comment, và đặt một quy tắc: mọi thay đổi hành vi đã ship phải cập nhật spec trong cùng khoảng giao hàng, dù chỉ năm dòng.
Giữ gaps tách khỏi backlog. Gom chúng vào "hành vi chưa rõ," "hành vi không nhất quán," và "thiếu test," rồi xem nhanh hàng tuần để quyết điều gì quan trọng ngay bây giờ.
Nếu việc soạn thảo và lặp cảm thấy chậm, công cụ chat-based như Koder.ai có thể giúp bạn có bản nháp ban đầu nhanh. Mô tả tính năng, dán đoạn mã hoặc tên route chính, tinh chỉnh câu chữ trong cuộc trò chuyện, và xuất nguồn khi cần. Mục tiêu là tốc độ và rõ ràng chung, không phải quy trình to hơn.
Bắt đầu với một phần nhỏ, hiển thị cho người dùng (ví dụ: “đặt lại mật khẩu” hoặc “mời đồng đội”). Đọc routes/handlers để nắm các quy tắc và kết quả, rồi đọc luồng UI để ghi lại trải nghiệm người dùng thực tế (trạng thái bị disable, lỗi, chuyển hướng). Ghi lại bằng một mẫu nhất quán và lưu các điều chưa rõ trong một danh sách gaps riêng.
Mặc định: xem hành vi hiện tại của mã là nguồn sự thật và tài liệu hóa nó.
Nếu hành vi trông như vô ý hay không đồng nhất, đừng “sửa” trong spec — đánh dấu nó là một gap kèm bằng chứng (nơi bạn thấy và nó hoạt động thế nào), rồi lấy quyết định để cập nhật code hoặc spec.
Giữ cho nó nhàm và lặp lại. Một mẫu thực dụng là:
Cách này giúp spec dễ đọc và làm lộ các bất đồng nhanh hơn.
Viết quy tắc dưới dạng yêu cầu người dùng, chứ không phải chú thích mã.
Ví dụ:
Ghi lại điều gì sẽ gây ra lỗi và người dùng sẽ thấy gì khi lỗi đó xảy ra.
Tập trung vào những gì quan sát được:
Side effects quan trọng vì chúng ảnh hưởng tới các tính năng khác và kỳ vọng hỗ trợ/ops.
Nếu UI chặn điều gì mà API cho phép (hoặc ngược lại), ghi nó thành một gap cho đến khi quyết định được đưa ra.
Ghi lại:
Rồi đồng ý một quy tắc duy nhất và cập nhật cả code lẫn spec cho khớp.
Giữ danh sách gaps ngắn và có bằng chứng. Mỗi mục nên có:
Tránh lập lịch hay biến nó thành backlog thứ hai.
Ghi rõ thay vì giấu:
Bao gồm:
Chúng thường là nơi phát sinh lỗi và bất ngờ.
Ngắn gọn: đọc trong 20–30 phút với một engineer và một product person.
Biến các phát biểu thành câu hỏi có/không (ví dụ: “Khi người dùng gọi route này, chúng ta luôn trả 403 nếu không có session chứ?”). Đồng ý về từ vựng theo UI (nhãn, thông báo) để mọi người hiểu cùng một nghĩa.
Đặt spec gần mã và làm cho cập nhật là một phần của việc phát hành.
Mặc định thực tế:
Mục tiêu là sửa đổi nhỏ, thường xuyên — không phải viết lại lớn.