Stage 7 · Master
Building the HTTP API with Gin
Consistent Error Handling
Every Fieldwork endpoint returns the same error envelope, while domain errors map cleanly to HTTP without leaking internals.
Why Error Shape Is an API Contract
Teams often treat error responses as leftovers: whatever message fell out of the handler, plus maybe a status code. That works until clients try to build against it. Then every endpoint has its own little dialect. One returns error, another message, a third returns a raw database failure, and frontends end up hard-coding special cases. Fieldwork treats error shape as part of the API contract from the start because the system will have many endpoints and the client experience should not degrade the moment something goes wrong.
The rejected approach was to let handlers write JSON ad hoc based on immediate context. That is faster for the first three endpoints and worse for every endpoint after that. I also rejected surfacing raw error strings from lower layers. Domain and repository errors are rich enough for server-side logs, but many of them are noisy, unstable, or security-revealing when returned directly to clients.
A frontend needs to know whether a task was not found, a request was invalid, or the caller lacked access. It does not need 'pq: duplicate key value violates unique constraint idx_projects_tenant_workspace_status_created'.
The Envelope Fieldwork Returns
Fieldwork standardizes on one JSON envelope with three stable ideas: a machine-readable code, a human-readable message, and request metadata that helps correlate incidents. Validation failures optionally add field-level details, but the outer shape stays the same. That consistency means frontend code, docs, and monitoring can all reason about errors without endpoint-specific parsing rules.
| Failure type | HTTP status | Error code | Client-facing message |
|---|---|---|---|
| Malformed JSON / invalid DTO | 400 | validation_failed | Request body failed validation |
| Missing workspace membership | 403 | forbidden | You do not have access to this workspace |
| Unknown task | 404 | task_not_found | Task not found |
| Uniqueness conflict | 409 | project_slug_conflict | A project with this slug already exists in the workspace |
| Unexpected server failure | 500 | internal_error | The server could not complete the request |
Mapping Domain Errors Without Losing Context
A consistent envelope needs a mapping layer between domain failures and HTTP semantics. Fieldwork uses sentinel or typed errors in the service and repository layers, then centralizes the translation in one writer. That keeps handlers from open-coding status selection every time. More importantly, it stops infrastructure details from leaking. A repository can wrap a pgx error with query context for logs while the response writer still turns it into a stable 409 or 500 for the client.
One subtle rule matters here: domain errors should be meaningful in domain language, not HTTP language. ErrTaskNotFound is fine. ErrHTTP404Task is not. Services should not know or care how a transport layer serializes failure. The handler layer owns the HTTP mapping because it owns the client contract.
Frontends and SDKs should branch on codes like task_not_found or validation_failed. Messages can improve over time without becoming the control surface for client logic.
Logging vs Returning Errors
A common mistake is logging the same error at every layer and returning it anyway, producing a noisy incident trail full of duplicates. Fieldwork treats errors as either handled or escalated. If a handler maps a known domain failure to a 404 or 403, it usually does not log at error level because the result is expected business behavior. Unexpected failures get logged once, near the boundary where the request is being terminated, with request and tenant context already attached. That keeps logs useful instead of repetitive.
Validation errors are another special case. The API should tell the client what was wrong, but that does not mean every bad payload deserves an error log. Usually it is enough to return a 400 with structured details and maybe emit a lower-severity request completion log. Save error-level noise for failures operators actually need to investigate.
- Return one stable JSON error envelope across every endpoint.
- Map domain and repository errors centrally instead of per handler.
- Keep HTTP semantics out of service-layer error names.
- Log unexpected failures once, with request context, near the transport boundary.
The Decision
Fieldwork standardizes error handling because inconsistency here becomes client pain immediately. The decision was to use one envelope, one mapping layer, and stable machine-readable codes while keeping rich wrapped errors on the server side only. The rejected alternatives — ad hoc JSON, raw lower-layer messages, and duplicated logging — all create more noise than signal in a system that already has enough moving parts.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.