Should I move my API error bodies to the RFC 7807 problem+json format?
Question
We have a platform where each service (some Laravel, some Go) returns errors in a slightly different body: one uses `{"error": "..."}`, another `{"message": "...", "code": 42}`, a third a nested `errors` object. The client-side teams have to parse each service's errors separately and it's become unsustainable. Would it make sense to move the error bodies to the RFC 7807 `application/problem+json` format to establish a single contract, or does it just add unnecessary standards overhead?
Answer
Short answer: yes, standardize.
Short answer
But two conditions: base it on RFC 9457 (which updates/obsoletes 7807) rather than 7807 itself, and enforce it in one central layer, not per-endpoint by hand.
Why
-
7807 is now RFC 9457. Same shape, refreshed in 2023. Reference 9457 in new work. The fields are the same:
type(URI),title,status,detail,instance— plus as many extra (extension) members as you like. -
The real win is the uniform envelope, not the field names. Client teams now parse a single shape across every service. That was exactly your pain, and it’s the whole value of the standard. So yes, it’s worth doing.
What to do
-
Set the content type correctly: application/problem+json. So clients and intermediary proxies can distinguish it from a normal body. Forgetting this header is implementing the standard halfway.
-
type is a stable machine key — treat it as a contract. Use a URN or a documentation URL. Clients branch on
type, not on human-readable fields liketitle/detail. Don’t leak stack traces or internals intodetail; that’s a human-readable sentence for the caller.{ "type": "https://api.example.com/problems/insufficient-funds", "title": "Insufficient funds", "status": 422, "detail": "Wallet 7f3a has balance 12.00, needs 50.00.", "instance": "/wallets/7f3a/withdrawals/9910" } -
Enforce it in one place — the exception handler / middleware. In Laravel, produce it in the
Handler’srenderlayer (since Laravel 11, that’swithExceptions()->render()inbootstrap/app.phpinstead); in Go, in a central “error → problem” mapper. Write it per-endpoint and it drifts over time, defeating the point of the standard. -
Validation errors need an extension. 9457 doesn’t define a field-level error list. Add an
errorsarray extension for validation and fix that shape once across all services — otherwise it’ll diverge per service again. -
Plan the migration to be backward compatible. Existing clients may expect the old body. Switching the server side is easy, but clients don’t update overnight. Tie the new
problem+jsonformat to an API version, or during a transition window return both fields (e.g. the oldmessagealongside the newdetail) for a while, and drop the old one as clients migrate. -
Don’t over-standardize everything. problem+json is for error bodies; it doesn’t change the shape of successful responses. And automate the docs too: reference the
typecatalog in your OpenAPI schema so clients can see which error types to expect from the contract itself.
Bottom line: personally I’d base it on 9457, define a versionable URI scheme for type, consolidate production in a single exception handler, and define a shared errors extension for validation. I’d also migrate gradually — filling out the type catalog and moving clients over as I go — rather than in one big bang. The value of the standard is in the discipline: one envelope, one production point, type managed like a contract.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.