Stage 7 · Master
Identity Service: HTTP API Layer
Consistent Error Handling
Why failure shape is part of the API contract, how Meridian's existing APIError type should be mapped from domain failures, and why stable codes matter more than ad hoc messages when several services must feel like one platform.
Why Errors Are Contracts
A backend API is judged most harshly when something goes wrong. Success paths can survive minor inconsistencies for a while; error paths cannot. If one endpoint returns {error: 'bad request'}, another returns {message: 'forbidden'}, and a third leaks a raw SQL string, client code becomes a pile of special cases and operators lose the ability to reason about failures systematically. Consistent error handling is therefore not polishing. It is core API design.
The deeper lesson is that error handling has two audiences. Clients need stable machine-readable semantics so they can branch correctly. Humans need enough context to debug. Those goals are related but not identical. That is why a good backend often returns a restrained public error shape while logging a richer internal error chain with stack, query, and request context.
The Platform Failure Shape
Meridian already has the failure envelope implemented in libs/platform/errors. That is important because it prevents each service from inventing its own format later. The current repository does not yet use the type in handlers, but the contract exists and should remain the only public failure shape once business endpoints are added.
| Failure class | HTTP status | Public code | Why the code matters |
|---|---|---|---|
| Malformed JSON or invalid DTO | 400 | validation_failed | Clients can highlight field issues without parsing English prose |
| Unknown tenant membership | 404 or 403 depending on endpoint policy | membership_not_found or forbidden | The caller can distinguish missing data from missing permission |
| Conflicting uniqueness rule | 409 | membership_already_exists | Clients can handle retries and UX messaging predictably |
| Unexpected server failure | 500 | internal_error | Stable fallback semantics preserve API sanity under incidents |
Mapping Domain Failures
The service layer should describe failure in domain language, not in HTTP language. ErrMembershipAlreadyExists is a good domain error. ErrHTTP409Membership is not. The handler or a dedicated response-mapping helper is where domain semantics meet transport semantics. That separation keeps the business layer reusable and the HTTP layer authoritative about status codes and wire shape.
Validation deserves special treatment because clients often need field-level feedback, not just a generic 400. That is why the failure contract includes details. The outer shape remains the same, which is crucial. Uniform failure envelopes let frontend and SDK code rely on one parsing model even when individual endpoints report different field names.
Logging vs Returning
A useful rule is to log unexpected failures once, near the boundary where the request is being terminated. Expected business outcomes such as not found, forbidden, or validation failed usually should not be logged at error level because they are not operator incidents. Logging every error at every layer creates duplicate noise and makes real production failures harder to identify.
Trace IDs are the bridge between public failure and private diagnosis. A client or operator sees trace_id in the response. The logs contain the same value plus richer context such as tenant ID, route, actor, and wrapped error chain. That split preserves both usability and security: clients get enough to report a problem, but not an accidental leak of SQL, internal stack state, or third-party library messages.
Frontend logic should branch on stable codes like validation_failed or membership_already_exists. Human-readable messages may improve over time without becoming the programmatic contract.
Current Repository Status
The current meridian services expose only /healthz and do not yet construct errors.APIError values. This chapter is a forward-looking HTTP design specification grounded in the existing shared type, not a walkthrough of already-implemented identity-service handlers.
- Treat failure shape as part of the public contract, not as leftover JSON.
- Map domain errors centrally into HTTP status codes and APIError values.
- Use details for structured validation feedback while keeping the outer error envelope uniform.
- Log unexpected failures once with trace correlation; do not spam logs for normal business rejections.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.