Claude Code cho scaffold Go API: xác định mẫu handler-service-error rõ ràng, rồi sinh endpoint mới giữ tính nhất quán trên toàn bộ API Go của bạn.

API Go thường bắt đầu sạch: vài endpoint, một hai người, và mọi thứ sống trong đầu mọi người. Rồi API lớn dần, tính năng ra gấp, và những khác biệt nhỏ len lỏi. Mỗi cái một mình có vẻ vô hại, nhưng cộng lại thì làm chậm mọi thay đổi sau này.
Một ví dụ phổ biến: một handler decode JSON vào struct và trả 400 với thông tin hữu ích, handler khác trả 422 với cấu trúc khác, handler thứ ba log lỗi theo định dạng khác. Không cái nào làm vỡ biên dịch. Chúng chỉ tạo ra quyết định liên tục và sửa lại nhỏ mỗi khi bạn thêm thứ gì đó.
Bạn sẽ thấy sự lộn xộn ở những chỗ như:
CreateUser, AddUser, RegisterUser) khiến tìm kiếm khó khăn."Scaffolding" ở đây nghĩa là một khuôn mẫu có thể lặp lại cho công việc mới: code đặt ở đâu, mỗi layer làm gì, và phản hồi trông như thế nào. Ít hơn là sinh nhiều code, và nhiều hơn là khóa một hình dạng nhất quán.
Công cụ như Claude có thể giúp bạn scaffold endpoint nhanh, nhưng chúng chỉ hữu ích khi bạn coi pattern là quy tắc. Bạn định nghĩa quy tắc, review mọi diff, và chạy test. Mô hình chỉ điền phần chuẩn; nó không được phép định nghĩa lại kiến trúc của bạn.
API Go dễ mở rộng khi mọi request theo cùng một đường đi. Trước khi bắt đầu sinh endpoint, chọn một phân tách lớp và tuân theo nó.
Nhiệm vụ của handler chỉ là HTTP: đọc request, gọi service, và ghi response. Nó không nên chứa quy tắc nghiệp vụ, SQL, hoặc logic "chỉ trường hợp này".
Service chịu trách nhiệm use case: quy tắc nghiệp vụ, quyết định, và phối hợp giữa repository hoặc cuộc gọi ngoài. Nó không nên biết các mối quan tâm HTTP như status code, header, hay cách hiển thị lỗi.
Data access (repository/store) chịu chi tiết persistency. Nó dịch ý định service thành SQL/query/transaction. Nó không nên áp dụng quy tắc nghiệp vụ ngoài tính toàn vẹn dữ liệu cơ bản, và không nên định hình phản hồi API.
Một checklist phân tách giữ thực tế:
Chọn một quy tắc và đừng bẻ cong.
Một cách đơn giản:
Ví dụ: handler kiểm tra email có tồn tại và nhìn như email. Service kiểm tra email được phép và chưa được dùng.
Quyết định sớm xem service trả domain types hay DTOs.
Mặc định sạch: handlers dùng request/response DTOs, services dùng domain types, và handler map domain sang response. Điều đó giữ service ổn định ngay cả khi hợp đồng HTTP thay đổi.
Nếu mapping cảm thấy nặng, vẫn giữ nhất quán: để service trả domain type cộng lỗi typed, và giữ việc định dạng JSON trong handler.
Nếu bạn muốn các endpoint sinh tự động giống như cùng một người viết, khóa định dạng lỗi sớm. Sinh hoạt tốt nhất khi định dạng đầu ra không thể thương lượng: một shape JSON, một bản đồ status code, và một quy tắc cho những gì được tiết lộ.
Bắt đầu với một error envelope duy nhất mà mọi endpoint trả khi thất bại. Giữ nó nhỏ và dễ đoán:
{
"code": "validation_failed",
"message": "One or more fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address",
"age": "must be greater than 0"
}
},
"request_id": "req_01HR..."
}
Dùng code cho máy (ổn định và dễ dự đoán) và message cho con người (ngắn và an toàn). Đặt dữ liệu cấu trúc vào details. Với validation, một details.fields map đơn giản dễ sinh và dễ cho client hiển thị cạnh input.
Tiếp theo, viết ra bản đồ status code và tuân theo nó. Càng ít tranh luận cho mỗi endpoint, càng tốt. Nếu bạn muốn cả 400 và 422, hãy phân chia rõ ràng:
bad_json -> 400 Bad Request (JSON bị hỏng)validation_failed -> 422 Unprocessable Content (JSON đúng định dạng, nhưng trường không hợp lệ)not_found -> 404 Not Foundconflict -> 409 Conflict (khóa trùng, mismatch version)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server ErrorQuyết định cái gì log vs cái gì trả về. Một quy tắc tốt: client nhận thông điệp an toàn và request ID; logs chứa lỗi đầy đủ và context nội bộ (SQL, payload upstream, user IDs) mà bạn không bao giờ muốn rò rỉ.
Cuối cùng, chuẩn hóa request_id. Chấp nhận header ID đến nếu có (từ API gateway), nếu không thì tạo một cái ở edge (middleware). Gắn nó vào context, kèm vào logs, và trả lại trong mọi phản hồi lỗi.
Nếu muốn scaffold giữ nhất quán, cấu trúc thư mục của bạn phải nhàm chán và có thể lặp lại. Generator theo các pattern chúng nhìn thấy, nhưng sẽ trôi khi file rải rác hoặc tên thay đổi theo feature.
Chọn một convention đặt tên và không bẻ cong. Chọn một từ cho mỗi thứ và giữ nó: handler, service, repo, request, response. Nếu route là POST /users, đặt tên file và type quanh users và create (không thỉnh thoảng register, thỉnh thoảng addUser).
Một layout đơn giản phù hợp các lớp thường thấy:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
Quyết định nơi các type chia sẻ nằm, vì đó là nơi dự án thường trở nên rối. Một quy tắc hữu ích:
internal/apitypes (khớp JSON và nhu cầu validation).Nếu một type có tag JSON và được thiết kế cho client, coi nó là API type.
Giữ dependency của handler ở mức tối thiểu và làm quy tắc đó rõ ràng:
Viết một tài liệu pattern ngắn ở root repo (Markdown plain là đủ). Bao gồm cây thư mục, quy tắc đặt tên, và một flow ví dụ nhỏ (handler -> service -> repo, kèm file mỗi phần). Đây là tài liệu tham chiếu chính xác bạn dán vào generator để các endpoint mới luôn trùng cấu trúc.
Trước khi sinh mười endpoint, tạo một endpoint bạn tin tưởng. Đây là chuẩn vàng: file bạn trỏ vào và nói, "Code mới phải trông như này." Bạn có thể viết nó từ đầu hoặc refactor một cái sẵn cho đến khi khớp.
Giữ handler mỏng. Một cách giúp nhiều: đặt một interface giữa handler và service để handler phụ thuộc vào contract, không phải struct cụ thể.
Thêm vài comment ngắn trong endpoint tham chiếu chỉ nơi code sinh sau này có thể vấp. Giải thích quyết định (tại sao 400 vs 422, tại sao create trả 201, tại sao che lỗi nội bộ sau message chung). Bỏ comment chỉ tóm tắt code.
Khi endpoint tham chiếu hoạt động, trích helpers ra để mọi endpoint mới có ít cơ hội trôi. Helpers tái sử dụng phổ biến nhất thường là:
Đây là ví dụ "handler mỏng + interface":
type UserService interface {
CreateUser(ctx context.Context, in CreateUserInput) (User, error)
}
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
var in CreateUserRequest
if err := BindJSON(r, &in); err != nil {
WriteError(w, ErrBadJSON) // 400: malformed JSON
return
}
if err := Validate(in); err != nil {
WriteError(w, err) // 422: validation details
return
}
user, err := h.svc.CreateUser(r.Context(), in.ToInput())
if err != nil {
WriteError(w, err)
return
}
WriteJSON(w, http.StatusCreated, user)
}
Khóa pattern đó bằng vài test (thậm chí một table test nhỏ cho mapping lỗi). Generator làm việc tốt nhất khi có một mục tiêu sạch để bắt chước.
Tính nhất quán bắt đầu từ những gì bạn dán vào và cái bạn cấm. Với endpoint mới, cung cấp hai thứ:
Bao gồm handler, method service, request/response types, và helper chia sẻ endpoint dùng. Rồi nêu hợp đồng bằng từ ngữ rõ ràng:
POST /v1/widgets)Nói rõ phải khớp: tên, đường dẫn package, và helper functions (WriteJSON, BindJSON, WriteError, validator của bạn).
Một prompt chặt giúp tránh "refactor hữu ích". Ví dụ:
Using the reference endpoint below and the pattern notes, generate a new endpoint.
Contract:
- Route: POST /v1/widgets
- Request: {"name": string, "color": string}
- Response: {"id": string, "name": string, "color": string, "createdAt": string}
- Errors: invalid JSON -> 400; validation -> 422; duplicate name -> 409; unexpected -> 500
Output ONLY these files:
1) internal/http/handlers/widgets_create.go
2) internal/service/widgets.go (add method only)
3) internal/types/widgets.go (add types only)
Do not change: router setup, existing error format, existing helpers, or unrelated files.
Must use: package paths and helper functions exactly as in the reference.
Nếu bạn dùng tests, yêu cầu rõ ràng (và đặt tên file test). Nếu không, mô hình có thể bỏ qua test hoặc sáng tạo một setup test khác.
Làm một kiểm tra diff nhanh sau khi sinh. Nếu nó sửa helper chung, đăng ký router, hoặc định dạng lỗi chuẩn, từ chối đầu ra và nhắc lại quy tắc "không thay đổi" chặt hơn.
Đầu vào chỉ nhất quán bằng chính đầu vào của bạn. Cách nhanh nhất tránh code "gần đúng" là dùng một prompt template mỗi lần, với một snapshot ngắn từ repo.
Dán, điền chỗ trống:
You are editing an existing Go HTTP API.
CONTEXT
- Folder tree (only the relevant parts):
<paste a small tree: internal/http, internal/service, internal/repo, etc>
- Key types and patterns:
- Handler signature style: <example>
- Service interface style: <example>
- Request/response DTOs live in: <package>
- Standard error response JSON:
{
"error": {
"code": "invalid_argument",
"message": "...",
"details": {"field": "reason"}
}
}
- Status code map:
invalid_json -> 400
invalid_argument -> 422
not_found -> 404
conflict -> 409
internal -> 500
TASK
Add a new endpoint: <METHOD> <PATH>
- Handler name: <Name>
- Service method: <Name>
- Request JSON example:
{"name":"Acme"}
- Success response JSON example:
{"id":"123","name":"Acme"}
CONSTRAINTS
- No new dependencies.
- Keep functions small and single-purpose.
- Match existing naming, folder layout, and error style exactly.
- Do not refactor unrelated files.
ACCEPTANCE CHECKS
- Code builds.
- Existing tests pass (add tests only if the repo already uses them for handlers/services).
- Run gofmt on changed files.
FINAL INSTRUCTION
Before writing code, list any assumptions you must make. If an assumption is risky, ask a short question instead.
Cái này hiệu quả vì bắt buộc ba thứ: context block (cái đang có), constraints block (cái không được làm), và ví dụ JSON cụ thể (để shape không trôi). Final instruction là tấm lưới an toàn: nếu mô hình không chắc, nó sẽ hỏi trước khi ghi mã.
Giả sử bạn muốn thêm một endpoint "Create project". Mục tiêu: nhận name, áp vài quy tắc, lưu, và trả ID mới. Khó là giữ split handler-service-repo và kiểu lỗi JSON bạn đang dùng.
Một luồng nhất quán trông như:
Yêu cầu handler chấp nhận:
{ "name": "Roadmap", "owner_id": "u_123" }
Thành công trả 201 Created. ID nên đến từ một nguồn duy nhất mỗi lần. Ví dụ: để Postgres generate và repo trả lại:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
Hai đường thất bại thực tế:
Nếu validation fail (name thiếu hoặc quá ngắn), trả lỗi trường theo shape chuẩn và status bạn chọn:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
Nếu tên phải unique per owner và service thấy project tồn tại, trả 409 Conflict:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
Một quyết định giữ pattern sạch: handler kiểm tra "request shaped đúng chưa?" còn service sở hữu "được phép không?". Sự tách này làm cho các endpoint sinh tự động dễ đoán.
Cách nhanh nhất làm mất nhất quán là để generator tùy cơ ứng biến.
Một drift thường thấy là shape lỗi mới. Một endpoint trả {error: "..."}, endpoint khác trả {message: "..."}, endpoint thứ ba thêm nested object. Khóa điều này bằng một error envelope duy nhất và một bản đồ status code trong một nơi, rồi yêu cầu endpoint mới tái sử dụng chúng bằng import path và tên hàm. Nếu generator đề xuất field mới, coi đó là thay đổi API — phải làm change request, không phải tiện lợi.
Drift khác là handler phình to. Nó bắt đầu nhỏ: validate, rồi check permission, rồi query DB, rồi branching nghiệp vụ. Giữ một quy tắc: handler dịch HTTP thành input/output typed; service sở hữu quyết định; data access chịu query.
Mismatch đặt tên cũng tích tụ. Nếu một endpoint dùng CreateUserRequest và endpoint khác dùng NewUserPayload, bạn mất thời gian nối loại và viết glue. Chọn một quy ước tên và bác bỏ tên mới nếu không có lý do chính đáng.
Không bao giờ trả raw database errors cho client. Ngoài việc rò rỉ, nó tạo thông điệp lỗi và mã trạng thái không nhất quán. Bọc lỗi nội bộ, log nguyên nhân, và trả code public ổn định.
Tránh thêm thư viện mới "vì tiện". Mỗi validator, helper router, hoặc gói lỗi thêm vào là một phong cách khác phải khớp.
Những guardrail ngăn phần lớn hỏng hóc:
Nếu bạn không thể diff hai endpoint và thấy cùng hình dạng (imports, flow, error handling), thắt chặt prompt và sinh lại trước khi merge.
Trước khi merge bất cứ thứ gì sinh tự động, kiểm tra cấu trúc trước. Nếu hình dạng đúng, bug logic dễ phát hiện hơn.
Kiểm tra cấu trúc:
request_id.Kiểm tra hành vi:
Đối xử pattern của bạn như một hợp đồng chung, không phải sở thích. Giữ tài liệu "how we build endpoints" cạnh code, và duy trì một endpoint tham chiếu đầy đủ để minh họa toàn bộ cách làm.
Mở rộng tạo tự động theo từng lô nhỏ. Sinh 2–3 endpoint chạm các cạnh khác nhau (một GET đơn giản, một create có validation, một update có case not-found). Rồi dừng và tinh chỉnh. Nếu review liên tục thấy drift cùng kiểu, cập nhật baseline doc và endpoint tham chiếu trước khi sinh tiếp.
Vòng lặp bạn có thể lặp:
Nếu muốn vòng build-review khít hơn, một nền tảng vibe-coding như Koder.ai (koder.ai) có thể giúp scaffold và lặp nhanh trong workflow chat-driven, rồi export source khi nó khớp chuẩn của bạn. Công cụ ít quan trọng hơn quy tắc: baseline của bạn là người dẫn đường.
Khóa một khuôn mẫu có thể lặp lại ngay từ đầu: một tách lớp nhất quán (handler → service → data access), một phong cách envelope lỗi duy nhất, và một bản đồ mã trạng thái. Sau đó dùng một “endpoint tham chiếu” duy nhất làm ví dụ mà mọi endpoint mới phải sao chép.
Giữ handler chỉ lo về HTTP:
Nếu bạn thấy SQL, kiểm tra quyền, hoặc branching nghiệp vụ xuất hiện trong handler, hãy chuyển chúng vào service.
Đặt các quy tắc nghiệp vụ và quyết định vào service:
Service nên trả về domain results và lỗi typed — không mã HTTP, không định dạng JSON.
Cô lập các mối quan tâm liên quan đến persistency:
Tránh mã hóa định dạng phản hồi API hoặc ép buộc quy tắc nghiệp vụ trong repo ngoài các ràng buộc dữ liệu cơ bản.
Một mặc định đơn giản:
Ví dụ: handler kiểm tra email có tồn tại và trông như email; service kiểm tra email đó được phép và chưa được dùng.
Dùng một envelope lỗi tiêu chuẩn ở mọi nơi và giữ nó ổn định. Một dạng thực tế gồm:
code cho máy (ổn định)message cho con người (ngắn và an toàn)details cho các thông tin có cấu trúc (như lỗi trường)request_id để truy vếtĐiều này tránh các trường hợp đặc biệt phía client và giữ các endpoint sinh tự động dễ đoán.
Ghi rõ một bản đồ mã trạng thái và tuân theo nó mỗi lần. Một phân chia phổ biến:
Trả về lỗi công khai an toàn và log nguyên nhân thực sự bên trong.
code ổn định, message ngắn, kèm request_idĐiều này tránh rò rỉ nội bộ và ngăn các thông điệp lỗi khác nhau xuất hiện lung tung giữa các endpoint.
Tạo một endpoint “golden” bạn tin tưởng và yêu cầu các endpoint mới khớp nó:
BindJSON, WriteJSON, WriteError, ...)Rồi thêm vài test nhỏ (thậm chí table tests cho mapping lỗi) để khóa pattern.
Cho mô hình ngữ cảnh và ràng buộc chặt:
Sau khi sinh code, từ chối diff nếu nó “cải tiến” kiến trúc thay vì tuân theo baseline.
400 cho JSON sai định dạng (bad_json)422 cho lỗi validation (validation_failed)404 cho tài nguyên không tìm thấy (not_found)409 cho conflict (duplicate/version mismatch)500 cho lỗi bất ngờChìa khóa là nhất quán: không tranh luận từng endpoint.