Claude Code para scaffolding de APIs en Go: define un patrón limpio handler–service–error y genera endpoints nuevos que mantengan consistencia en tu API Go.

Las APIs en Go suelen empezar limpias: unos pocos endpoints, una o dos personas, y todo vive en la cabeza de todos. Luego la API crece, las funcionalidades se entregan bajo presión y se cuelan pequeñas diferencias. Cada una parece inocua, pero juntas ralentizan cualquier cambio futuro.
Un ejemplo común: un handler decodifica JSON a una struct y devuelve 400 con un mensaje útil, otro devuelve 422 con una forma distinta, y un tercero registra errores en otro formato. Nada de eso rompe la compilación. Simplemente crea toma de decisiones constante y pequeñas reescrituras cada vez que añades algo.
Notas de dónde se siente el desorden:
CreateUser, AddUser, RegisterUser) que hace que buscar sea más difícil.Por “scaffolding” aquí me refiero a una plantilla repetible para trabajo nuevo: dónde va el código, qué hace cada capa y qué aspecto tienen las respuestas. Es menos generar mucho código y más fijar una forma consistente.
Herramientas como Claude pueden ayudarte a scaffoldear nuevos endpoints rápido, pero solo siguen siendo útiles cuando tratas el patrón como una regla. Tú defines las reglas, revisas cada diff y ejecutas tests. El modelo rellena las partes estándar; no puede redefinir tu arquitectura.
Una API en Go es fácil de escalar cuando cada petición sigue el mismo camino. Antes de empezar a generar endpoints, elige una división de capas y mantenla.
El trabajo del handler es solo HTTP: leer la petición, llamar al servicio y escribir la respuesta. No debería contener reglas de negocio, SQL ni lógica de “solo este caso especial”.
El service se encarga del caso de uso: reglas de negocio, decisiones y orquestación entre repositorios o llamadas externas. No debería conocer preocupaciones HTTP como códigos de estado, cabeceras o cómo se renderizan los errores.
El acceso a datos (repositorio/store) se ocupa de los detalles de persistencia. Traduce la intención del service a SQL/consultas/transacciones. No debería imponer reglas de negocio más allá de la integridad básica de los datos ni moldear respuestas de API.
Una checklist de separación práctica:
Elige una regla y no la dobles.
Un enfoque simple:
Ejemplo: el handler comprueba que email está presente y tiene aspecto de email. El service comprueba que el email está permitido y no está ya en uso.
Decide pronto si los services devuelven tipos de dominio o DTOs.
Un defecto limpio es: los handlers usan DTOs de request/response, los services usan tipos de dominio y el handler mapea dominio a respuesta. Eso mantiene el service estable aunque cambie el contrato HTTP.
Si el mapeo parece pesado, mantenlo consistente de todos modos: que el service devuelva un tipo de dominio más un error tipado, y deja el moldeado JSON en el handler.
Si quieres que los endpoints generados parezcan escritos por la misma persona, fija las respuestas de error temprano. La generación funciona mejor cuando el formato de salida es no negociable: una sola forma JSON, un mapa de códigos y una regla sobre qué se expone.
Empieza con un único sobre de error que cada endpoint devuelve en fallos. Mantenlo pequeño y predecible:
{
"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..."
}
Usa code para las máquinas (estable y predecible) y message para humanos (corto y seguro). Pon datos estructurados opcionales en details. Para validación, un simple mapa details.fields es fácil de generar y fácil para que los clientes lo muestren junto a los inputs.
A continuación, escribe un mapa de códigos de estado y cúmplelo. Menos debate por endpoint, mejor. Si quieres tanto 400 como 422, haz la separación explícita:
bad_json -> 400 Bad Request (JSON malformado)validation_failed -> 422 Unprocessable Content (JSON bien formado, campos inválidos)not_found -> 404 Not Foundconflict -> 409 Conflict (clave duplicada, desajuste de versión)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server ErrorDecide qué registras frente a qué devuelves. Una buena regla: los clientes reciben un mensaje seguro y un request ID; los logs contienen el error completo y el contexto interno (SQL, payloads upstream, IDs de usuario) que nunca querrías filtrar.
Finalmente, estandariza request_id. Acepta un header de ID entrante si está presente (desde un API gateway); si no, genera uno en el borde (middleware). Adjúntalo al contexto, inclúyelo en los logs y devuélvelo en cada respuesta de error.
Si quieres que el scaffolding se mantenga consistente, la disposición de carpetas tiene que ser aburrida y repetible. Los generadores siguen patrones que pueden ver, pero se desvían cuando los archivos están dispersos o los nombres cambian por feature.
Elige una convención de nombres y no la dobles. Usa una palabra para cada cosa y manténla: handler, service, repo, request, response. Si la ruta es POST /users, nombra archivos y tipos alrededor de users y create (no a veces register, a veces addUser).
Una disposición simple que encaja con las capas:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
Decide dónde viven los tipos compartidos, porque aquí es donde los proyectos suelen volverse desordenados. Una regla útil:
internal/apitypes (coinciden con JSON y necesidades de validación).Si un tipo tiene etiquetas JSON y está diseñado para clientes, trátalo como tipo de API.
Mantén las dependencias de los handlers mínimas y explícitas:
Escribe un documento corto de patrón en la raíz del repo (Markdown simple está bien). Incluye el árbol de carpetas, reglas de nombres y un pequeño flujo de ejemplo (handler -> service -> repo, y en qué archivo va cada pieza). Esta es la referencia exacta que pegas en tu generador para que los nuevos endpoints coincidan con la estructura cada vez.
Antes de generar diez endpoints, crea uno que controles. Éste es el estándar de oro: el archivo al que señalas y dices, “el código nuevo debe verse así.” Puedes escribirlo desde cero o refactorizar uno existente hasta que coincida.
Mantén el handler delgado. Un movimiento que ayuda mucho: pon una interfaz entre el handler y el service para que el handler dependa de un contrato, no de una estructura concreta.
Añade pequeños comentarios en el endpoint de referencia solo donde el código generado podría confundirse en el futuro. Explica decisiones (por qué 400 vs 422, por qué el create devuelve 201, por qué ocultas errores internos detrás de un mensaje genérico). Evita comentarios que solo repitan el código.
Una vez que el endpoint de referencia funciona, extrae helpers para que cada nuevo endpoint tenga menos oportunidades de desviarse. Los helpers más reutilizables suelen ser:
Esto es lo que un “handler delgado + interfaz” puede parecer en la práctica:
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)
}
Fíjalo con un par de tests (incluso una pequeña tabla de tests para el mapeo de errores). La generación funciona mejor cuando tiene un objetivo limpio que imitar.
La consistencia comienza con lo que pegas y lo que prohíbes. Para un endpoint nuevo, da dos cosas:
Incluye el handler, el método del service, tipos de request/response y cualquier helper compartido que use el endpoint. Luego explica el contrato en términos simples:
POST /v1/widgets)Sé explícito sobre lo que debe coincidir: nombres, rutas de paquetes y funciones helper (WriteJSON, BindJSON, WriteError, tu validador).
Un prompt ajustado evita refactors “útiles”. Por ejemplo:
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.
Si usas tests, pídelos explícitamente (y nombra el archivo de test). De lo contrario, el modelo puede saltárselos o inventar una configuración de test.
Haz una revisión rápida del diff tras la generación. Si modificó helpers compartidos, el registro del router o tu respuesta de error estándar, rechaza la salida y reitera las reglas de “no cambiar” más estrictamente.
La salida es tan consistente como la entrada que le des. La forma más rápida de evitar código “casi correcto” es reutilizar una plantilla de prompt cada vez, con un pequeño snapshot de contexto tomado de tu repo.
Copia, pega y rellena los marcadores:
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.
Esto funciona porque fuerza tres cosas: un bloque de contexto (qué existe), un bloque de restricciones (qué no hacer) y ejemplos JSON concretos (para que las formas no se desvíen). La instrucción final es tu red de seguridad: si el modelo no está seguro, debe decirlo antes de comprometerse a un nuevo patrón.
Supongamos que quieres añadir un endpoint “Create project”. El objetivo es simple: aceptar un nombre, aplicar unas pocas reglas, guardarlo y devolver un ID nuevo. La parte difícil es mantener la misma separación handler-service-repo y el mismo JSON de error que ya usas.
Un flujo consistente se ve así:
Aquí está la petición que acepta el handler:
{ "name": "Roadmap", "owner_id": "u_123" }
En éxito, devuelve 201 Created. El ID debe venir de un único lugar siempre. Por ejemplo, deja que Postgres lo genere y que el repo lo devuelva:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
Dos rutas de fallo realistas:
Si falla la validación (nombre faltante o demasiado corto), devuelve un error por campo usando tu forma estándar y el código de estado elegido:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
Si el nombre debe ser único por propietario y el service encuentra un proyecto existente, devuelve 409 Conflict:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
Una decisión que mantiene el patrón limpio: el handler comprueba “¿esta petición tiene la forma correcta?” mientras que el service es dueño de “¿está esto permitido?”. Esa separación hace que los endpoints generados sean previsibles.
La forma más rápida de perder consistencia es permitir que el generador improvise.
Un desvío común es una nueva forma de error. Un endpoint devuelve {error: "..."}, otro devuelve {message: "..."} y un tercero añade un objeto anidado. Arregla esto manteniendo un único sobre de error y un mapa de códigos en un solo lugar, y exige que los nuevos endpoints los reutilicen por ruta de import y nombre de función. Si el generador propone un nuevo campo, trátalo como un cambio de API, no como una conveniencia.
Otro desvío es el engorde del handler. Empieza pequeño: valida, luego comprueba permisos, luego consulta la BD, luego ramifica en reglas de negocio. Pronto cada handler se ve distinto. Mantén una regla: los handlers traducen HTTP a entradas y salidas tipadas; los services son dueños de las decisiones; el acceso a datos es dueño de las consultas.
Los desajustes de nombres también se acumulan. Si un endpoint usa CreateUserRequest y otro NewUserPayload, perderás tiempo buscando tipos y escribiendo adaptadores. Escoge un esquema de nombres y rechaza nombres nuevos salvo razón sólida.
Nunca devuelvas errores crudos de la base de datos a los clientes. Además de filtrar detalles, crea mensajes y códigos de estado inconsistentes. Envuelve los errores internos, registra la causa y devuelve un código público estable.
Evita añadir librerías nuevas “solo por conveniencia”. Cada validador extra, helper de router o paquete de errores se convierte en otro estilo que igualar.
Guardarraíles que previenen la mayoría de roturas:
Si no puedes diffear dos endpoints y ver la misma forma (imports, flujo, manejo de errores), ajusta el prompt y regenera antes de mergear.
Antes de mergear algo generado, comprueba la estructura primero. Si la forma es correcta, los bugs lógicos son más fáciles de detectar.
Comprobaciones de estructura:
request_id.Comprobaciones de comportamiento:
Trata tu patrón como un contrato compartido, no como una preferencia. Mantén el documento “cómo construimos endpoints” junto al código y conserva un endpoint de referencia que muestre el enfoque completo.
Aumenta la generación en lotes pequeños. Genera 2 o 3 endpoints que cubran distintos bordes (una lectura simple, un create con validación, una actualización con un caso de not-found). Luego para y refina. Si las revisiones siguen encontrando la misma deriva de estilo, actualiza el doc base y el endpoint de referencia antes de generar más.
Un ciclo que puedes repetir:
Si quieres un bucle build-review más ajustado, una plataforma vibe-coding como Koder.ai (koder.ai) puede ayudarte a scaffoldear e iterar rápido en un flujo de trabajo guiado por chat, y luego exportar el código una vez que coincida con tu estándar. La herramienta importa menos que la regla: tu baseline debe seguir estando al mando.
Bloquea una plantilla repetible desde temprano: una división de capas consistente (handler → service → data access), un sobres de error único y un mapa de códigos de estado. Luego usa un único “endpoint de referencia” como ejemplo que cada nuevo endpoint debe copiar.
Mantén los handlers centrados en HTTP:
Si ves SQL, comprobaciones de permisos o ramificaciones de negocio en un handler, trasládalas al servicio.
Coloca reglas de negocio y decisiones en el service:
El service debe devolver resultados de dominio y errores tipados: sin códigos HTTP, sin dar forma a JSON.
Mantén aisladas las preocupaciones de persistencia:
Evita codificar formatos de respuesta de API o imponer reglas de negocio en el repositorio, salvo la integridad básica de los datos.
Un valor por defecto simple:
Ejemplo: el handler comprueba que email está presente y parece un email; el service comprueba que esté permitido y que no esté ya en uso.
Usa un único sobre de error estándar en todas partes y mantenlo estable. Una forma práctica es:
code para máquinas (estable)message para humanos (corto y seguro)details para extras estructurados (por ejemplo errores por campo)request_id para trazabilidadEsto evita casos especiales en el cliente y hace que los endpoints generados sean previsibles.
Escribe un mapa de códigos de estado y síguelo siempre. Una división común:
Devuelve errores públicos seguros y consistentes, y registra la causa real internamente.
code estable, message corto, más request_idAsí evitas filtrar detalles internos y diferencias aleatorias en los mensajes de error entre endpoints.
Crea un endpoint “golden” de referencia que controles y exige que los nuevos endpoints se ajusten a él:
BindJSON, WriteJSON, WriteError, etc.)Añade un par de tests pequeños (incluso tests de tabla para el mapeo de errores) para fijar el patrón.
Dale al modelo contexto y restricciones estrictas:
Tras la generación, rechaza diffs que “mejoren” la arquitectura en lugar de seguir la base.
400 para JSON malformado (bad_json)422 para fallos de validación (validation_failed)404 para recursos inexistentes (not_found)409 para conflictos (duplicados/desajustes de versión)500 para fallos inesperadosLa clave es la consistencia: no debatir por endpoint.