KoderKoder.ai
PricingEnterpriseEducationFor investors
Log inGet started

Product

PricingEnterpriseFor investors

Resources

Contact usSupportEducationBlog

Legal

Privacy PolicyTerms of UseSecurityAcceptable Use PolicyReport Abuse

Social

LinkedInTwitter
Koder.ai
Language

© 2026 Koder.ai. All rights reserved.

Home›Blog›Claude Code for Go API scaffolding: consistent handlers and services
Jan 01, 2026·6 min

Claude Code for Go API scaffolding: consistent handlers and services

Claude Code for Go API scaffolding: define one clean handler-service-error pattern, then generate new endpoints that stay consistent across your Go API.

Claude Code for Go API scaffolding: consistent handlers and services

Why Go APIs get messy when patterns aren't fixed early

Go APIs usually start clean: a few endpoints, one or two people, and everything lives in everyone's head. Then the API grows, features ship under pressure, and small differences sneak in. Each one feels harmless, but together they slow every future change.

A common example: one handler decodes JSON into a struct and returns 400 with a helpful message, another returns 422 with a different shape, and a third logs errors in a different format. None of that breaks compilation. It just creates constant decision-making and tiny rewrites every time you add something.

You feel the mess in places like:

  • Different error bodies across endpoints, so clients need special cases.
  • Services that sometimes return raw database errors and sometimes "friendly" ones.
  • Naming drift (CreateUser, AddUser, RegisterUser) that makes searching harder.
  • Validation that moves around, so the same bugs keep returning.

"Scaffolding" here means a repeatable template for new work: where code goes, what each layer does, and what responses look like. It's less about generating lots of code and more about locking in a consistent shape.

Tools like Claude can help you scaffold new endpoints fast, but they only stay helpful when you treat the pattern as a rule. You define the rules, you review every diff, and you run tests. The model fills in the standard parts; it doesn't get to redefine your architecture.

Pick one layer split: handler, service, and data access

A Go API stays easy to grow when every request follows the same path. Before you start generating endpoints, pick one layer split and stick to it.

Responsibilities, kept simple

The handler's job is HTTP only: read the request, call the service, and write the response. It shouldn't contain business rules, SQL, or "just this one special case" logic.

The service owns the use case: business rules, decisions, and orchestration across repositories or external calls. It shouldn't know about HTTP concerns like status codes, headers, or how errors are rendered.

Data access (repository/store) owns persistence details. It translates service intent into SQL/queries/transactions. It shouldn't enforce business rules beyond basic data integrity, and it shouldn't shape API responses.

A separation checklist that stays practical:

  • Handler: parse input, call service, map errors to HTTP, write JSON
  • Service: apply business rules, call repos, return domain results or typed errors
  • Data access: execute queries, map rows to structs, return storage errors
  • Shared: common error types and response helpers
  • No layer skips another (handler never calls repo directly)

One rule for validation

Pick one rule and don't bend it.

A simple approach:

  • Handlers do "shape validation" (required fields, basic format).
  • Services do "meaning validation" (permissions, invariants, state).

Example: the handler checks that email is present and looks like an email. The service checks that the email is allowed and not already in use.

What moves between layers

Decide early whether services return domain types or DTOs.

A clean default is: handlers use request/response DTOs, services use domain types, and the handler maps domain to response. That keeps the service stable even when the HTTP contract changes.

If mapping feels heavy, keep it consistent anyway: have the service return a domain type plus a typed error, and keep the JSON shaping in the handler.

Define a standard error response and status code map

If you want generated endpoints to feel like they were written by the same person, lock down error responses early. Generation works best when the output format is non-negotiable: one JSON shape, one status-code map, and one rule for what gets exposed.

Start with a single error envelope that every endpoint returns on failure. Keep it small and predictable:

{
  "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..."
}

Use code for machines (stable and predictable) and message for humans (short and safe). Put optional structured data into details. For validation, a simple details.fields map is easy to generate and easy for clients to display next to inputs.

Next, write down a status code map and stick to it. The less debate per endpoint, the better. If you want both 400 and 422, make the split explicit:

  • bad_json -> 400 Bad Request (malformed JSON)
  • validation_failed -> 422 Unprocessable Content (well-formed JSON, invalid fields)
  • not_found -> 404 Not Found
  • conflict -> 409 Conflict (duplicate key, version mismatch)
  • unauthorized -> 401 Unauthorized
  • forbidden -> 403 Forbidden
  • internal -> 500 Internal Server Error

Decide what you log vs what you return. A good rule: clients get a safe message and a request ID; logs get the full error and internal context (SQL, upstream payloads, user IDs) that you would never want to leak.

Finally, standardize request_id. Accept an incoming ID header if present (from an API gateway), otherwise generate one at the edge (middleware). Attach it to the context, include it in logs, and return it in every error response.

Folder layout and naming that make generation predictable

If you want scaffolding to stay consistent, your folder layout has to be boring and repeatable. Generators follow patterns they can see, but they drift when files are scattered or names change per feature.

Pick one naming convention and don't bend it. Choose one word for each thing and keep it: handler, service, repo, request, response. If the route is POST /users, name files and types around users and create (not sometimes register, sometimes addUser).

A simple layout that matches the usual layers:

internal/
  httpapi/
    handlers/
    users_handler.go
  services/
    users_service.go
  data/
    users_repo.go
  apitypes/
    users_types.go

Decide where shared types live, because this is where projects often get messy. One useful rule:

  • API request/response types live in internal/apitypes (match JSON and validation needs).
  • Domain types live closer to the service layer (match business rules).

If a type has JSON tags and is designed for clients, treat it as an API type.

Keep handler dependencies minimal and make that rule explicit:

  • Handlers import only: routing/http, context, apitypes, and services
  • Services import: domain types and data access
  • Data access imports: database driver and query helpers
  • No handler imports the database package directly

Write a short pattern doc in the repo root (plain Markdown is fine). Include the folder tree, naming rules, and one tiny example flow (handler -> service -> repo, plus which file each piece belongs in). This is the exact reference you paste into your generator so new endpoints match the structure every time.

Create one reference endpoint that sets the pattern

Add safe rollback points
Capture a known-good state before bigger refactors or batch-generated endpoints.
Take Snapshot

Before generating ten endpoints, create one endpoint you trust. This is the gold standard: the file you can point to and say, "New code must look like this." You can write it from scratch or refactor an existing one until it matches.

Keep the handler thin. One move that helps a lot: put an interface between the handler and the service so the handler depends on a contract, not a concrete struct.

Add small comments in the reference endpoint only where future generated code might stumble. Explain decisions (why 400 vs 422, why create returns 201, why you hide internal errors behind a generic message). Skip comments that just restate code.

Once the reference endpoint works, extract helpers so every new endpoint has fewer chances to drift. The most reusable helpers are usually:

  • Bind JSON and handle malformed bodies
  • Validate input and return field errors
  • Write JSON responses consistently
  • Map domain errors to HTTP status codes

Here's what "thin handler + interface" can look like in practice:

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)
}

Lock it in with a couple of tests (even a small table test for error mapping). Generation works best when it has one clean target to imitate.

Step by step: have Claude generate a new endpoint that matches

Consistency starts with what you paste in and what you forbid. For a new endpoint, give two things:

  1. Your reference endpoint (the "perfect" example)
  2. A short pattern note that names the packages, functions, and helpers it must follow

1) Provide context first (reference + rules)

Include the handler, service method, request/response types, and any shared helpers the endpoint uses. Then state the contract in plain terms:

  • Route + method (example: POST /v1/widgets)
  • Request JSON fields (required vs optional)
  • Response JSON shape
  • Error cases and status codes
  • Files you expect back (and only those files)

Be explicit about what must match: naming, package paths, and helper functions (WriteJSON, BindJSON, WriteError, your validator).

2) Ask for output in the exact shape you want

A tight prompt prevents "helpful" refactors. For example:

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.

If you use tests, request them explicitly (and name the test file). Otherwise the model may skip them or invent a test setup.

Do a quick diff check after generation. If it modified shared helpers, router registration, or your standard error response, reject the output and restate the "do not change" rules more strictly.

A reusable prompt template for consistent endpoint scaffolds

The output is only as consistent as the input you give. The fastest way to avoid "almost right" code is to reuse one prompt template every time, with a small context snapshot pulled from your repo.

Prompt template

Copy, paste, and fill the placeholders:

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.

This works because it forces three things: a context block (what exists), a constraints block (what not to do), and concrete JSON examples (so shapes don't drift). The final instruction is your safety catch: if the model is unsure, it should say so before it commits you to a new pattern.

Realistic example: adding a Create endpoint without breaking the style

Own the code you ship
Generate, review, then export the source when the structure matches your repo rules.
Export Code

Say you want to add a "Create project" endpoint. The goal is simple: accept a name, enforce a few rules, store it, and return a new ID. The tricky part is keeping the same handler-service-repo split and the same error JSON you already use.

A consistent flow looks like this:

  • Handler: bind JSON, do basic field validation, call the service
  • Service: apply business rules (like uniqueness), call the repo, return a domain result
  • Repo: write to Postgres, return the generated ID

Here's the request the handler accepts:

{ "name": "Roadmap", "owner_id": "u_123" }

On success, return 201 Created. The ID should come from one place every time. For example, let Postgres generate it and have the repo return it:

{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }

Two realistic failure paths:

If validation fails (missing or too short name), return a field-level error using your standard shape and your chosen status code:

{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }

If the name must be unique per owner and the service finds an existing project, return 409 Conflict:

{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }

One decision that keeps the pattern clean: the handler checks "is this request shaped correctly?" while the service owns "is this allowed?" That separation makes generated endpoints predictable.

Common mistakes that break consistency (and how to avoid them)

The fastest way to lose consistency is to let the generator improvise.

One common drift is a new error shape. One endpoint returns {error: "..."}, another returns {message: "..."}, and a third adds a nested object. Fix this by keeping a single error envelope and a single status code map in one place, then requiring new endpoints to reuse them by import path and function name. If the generator proposes a new field, treat it like an API change request, not a convenience.

Another drift is handler bloat. It starts small: validate, then check permissions, then query the DB, then branch on business rules. Soon every handler looks different. Keep one rule: handlers translate HTTP into typed inputs and outputs; services own decisions; data access owns queries.

Naming mismatches also add up. If one endpoint uses CreateUserRequest and another uses NewUserPayload, you'll waste time hunting types and writing glue. Pick a naming scheme and reject new names unless there's a strong reason.

Never return raw database errors to clients. Aside from leaking details, it creates inconsistent messages and status codes. Wrap internal errors, log the cause, and return a stable public error code.

Avoid adding new libraries "just for convenience". Each extra validator, router helper, or error package becomes another style to match.

Guardrails that prevent most breakage:

  • Require new endpoints to reuse existing error types and helpers.
  • Keep handlers free of business rules and database access.
  • Enforce one naming convention for request and response structs.
  • Map internal errors to public error codes, never raw errors.
  • Add dependencies only with a clear, written reason.

If you can't diff two endpoints and see the same shape (imports, flow, error handling), tighten the prompt and regenerate before merging.

Quick checklist before you merge a generated endpoint

Ship and test faster
Host your API after scaffolding so you can test real client behavior early.
Deploy App

Before you merge anything generated, check structure first. If the shape is right, logic bugs are easier to spot.

Structure checks:

  • Handler flow is consistent: bind input, validate, call service, map domain errors to HTTP, write the response.
  • Service code contains business rules only: no direct HTTP or JSON work.
  • Success responses match your house style (either a shared envelope everywhere or direct JSON everywhere).
  • Error responses are uniform: same JSON fields, same codes, same request_id behavior.
  • Naming and placement look boring: file names, function names, and route names match existing endpoints, and everything is gofmt-formatted.

Behavior checks:

  • Run tests and add at least one small test for a new handler/service branch.
  • Confirm validation failures return the same code and status as similar endpoints.
  • Trigger one known service error (like "not found" or "conflict") and confirm the HTTP status and JSON shape.
  • Scan for copy-paste leftovers: wrong route path, wrong log message, mismatched DTO names.
  • Build and run the server locally once to ensure wiring and imports are correct.

Next steps: standardize the pattern, then scale generation safely

Treat your pattern as a shared contract, not a preference. Keep the "how we build endpoints" doc next to your code, and maintain one reference endpoint that shows the full approach end to end.

Scale up generation in small batches. Generate 2 to 3 endpoints that hit different edges (a simple read, a create with validation, an update with a not-found case). Then stop and refine. If reviews keep finding the same style drift, update the baseline doc and the reference endpoint before generating more.

A loop you can repeat:

  • Write down the baseline: file names, function names, request/response structs, error codes, and where validation happens.
  • Keep one endpoint "golden," and update it first whenever the pattern changes.
  • Batch-generate a few endpoints, review for consistency, then adjust the prompt and pattern doc.
  • Refactor older endpoints in chunks and keep a rollback path if behavior changes.
  • Track one metric for a week: time to add an endpoint, bug rate after merge, or review time.

If you want a tighter build-review loop, a vibe-coding platform like Koder.ai (koder.ai) can help you scaffold and iterate quickly in a chat-driven workflow, then export the source code once it matches your standard. The tool matters less than the rule: your baseline stays in charge.

FAQ

What’s the fastest way to stop a Go API from getting inconsistent?

Lock down a repeatable template early: a consistent layer split (handler → service → data access), one error envelope, and a status-code map. Then use a single “reference endpoint” as the example every new endpoint must copy.

What should a handler do (and not do)?

Keep handlers HTTP-only:

  • Bind/parse the request
  • Do basic “shape validation” (required fields, simple formats)
  • Call the service
  • Map typed errors to HTTP status codes
  • Write JSON using shared helpers

If you see SQL, permission checks, or business branching in a handler, push that into the service.

What belongs in the service layer?

Put business rules and decisions in the service:

  • Permissions and access rules
  • Invariants (state transitions, uniqueness rules, “allowed” checks)
  • Orchestration across repositories and external calls

The service should return domain results and typed errors—no HTTP status codes, no JSON shaping.

What belongs in the data access/repository layer?

Keep persistence concerns isolated:

  • SQL/queries and transactions
  • Mapping rows to structs
  • Returning storage errors (that the service can interpret)

Avoid encoding API response formats or enforcing business rules in the repo beyond basic data integrity.

Where should validation live?

A simple default:

  • Handlers validate request shape (missing fields, basic format)
  • Services validate meaning (permissions, invariants, state)

Example: handler checks email is present and looks like an email; service checks it’s allowed and not already in use.

What should a standard API error response look like?

Use one standard error envelope everywhere and keep it stable. A practical shape is:

  • code for machines (stable)
  • message for humans (short and safe)
  • details for structured extras (like field errors)
  • request_id for tracing

This avoids client-side special cases and keeps generated endpoints predictable.

How do I choose between 400 vs 422 vs 409 for errors?

Write down a status-code map and follow it every time. A common split:

  • 400 for malformed JSON (bad_json)
  • 422 for validation failures (validation_failed)
  • 404 for missing resources (not_found)
  • 409 for conflicts (duplicates/version mismatches)
  • 500 for unexpected failures

The key is consistency: no debating per endpoint.

Should I ever return raw database errors to clients?

Return safe, consistent public errors, and log the real cause internally.

  • Response: stable code, short message, plus request_id
  • Logs: full error details (SQL errors, upstream payloads, user IDs)

This prevents leaking internals and avoids random error message differences across endpoints.

What is a reference endpoint, and why do I need one?

Create one “golden” endpoint you trust and require new endpoints to match it:

  • Same flow (bind → validate → service → error map → JSON)
  • Same helpers (BindJSON, WriteJSON, WriteError, etc.)
  • Same folder layout and naming

Then add a couple of small tests (even table tests for error mapping) to lock the pattern in.

How do I prompt Claude to generate new endpoints without breaking my structure?

Give the model strict context and constraints:

  • Paste the reference endpoint and the pattern rules
  • Specify route, request/response JSON examples, and error cases
  • List exactly which files it may output
  • Explicitly say what it must not change (router setup, error format, helpers)

After generation, reject diffs that “improve” architecture instead of following the baseline.

Contents
Why Go APIs get messy when patterns aren't fixed earlyPick one layer split: handler, service, and data accessDefine a standard error response and status code mapFolder layout and naming that make generation predictableCreate one reference endpoint that sets the patternStep by step: have Claude generate a new endpoint that matchesA reusable prompt template for consistent endpoint scaffoldsRealistic example: adding a Create endpoint without breaking the styleCommon mistakes that break consistency (and how to avoid them)Quick checklist before you merge a generated endpointNext steps: standardize the pattern, then scale generation safelyFAQ
Share