skip to content

In a web framework, why route every violation set through one central converter instead of shaping error bodies inside each handler?

level: middleimportance: must knowfreq 62%

answer

  1. one shape, many endpoints
  2. handlers never touch violations
  3. registered on the failure hook
  4. wording lives in message keys
  5. change cost, not tidiness

basics

~20 s

Because the error body is a published contract, not handler detail. One converter registered on the framework's validation-failure hook gives every endpoint the same field names, codes, paths and redaction, and lets the shape change in a single place.

solid answer

~40 s

Frameworks expose a hook that runs when validation fails, before the handler body does. Registering one component there means handlers never touch violations at all: the converter receives the set, translates model property paths into wire names, resolves message keys, sorts for determinism, applies a redaction policy to rejected values, and hands the result to whatever writes the service's failure body. Shaping per handler drifts instead — `field` in one endpoint, `name` in the next, and a new endpoint that returns a bare sentence the client cannot highlight. The cost is that the converter must stay generic: endpoint-specific wording goes into the constraint's own message key, not into a branch over routes.

go deeper

for a junior

Remember that validation errors are formatted in one shared place, not per endpoint, and that the framework provides a hook for exactly that. Do not build an error body by hand in a handler.

for a middle

Explain the wiring and the steps: the failure hook intercepts, one component maps paths, messages, ordering and redaction, then the shared failure body writes it. Name what would drift without it.

for a senior

Talk about enforcement, not intent. A contract test across several endpoints, plus a review rule against hand-built bodies, is what keeps the single converter actually single.

for a principal

Frame it as change cost. Adding a documentation link or a request identifier to every validation response should be one edit and one test, on endpoints nobody had to enumerate first.

## The decision in one sentence A validation failure produces a structured set of violations; something has to turn that set into the bytes the caller reads. The question is *where* that turning happens — in each handler, in a helper the handlers remember to call, or in one component registered on the framework's error hook so that no handler is involved at all. The third arrangement is the one that holds up, because the error body is a **contract** and a contract with many authors stops being one. ## Three places it can live | Where | How it is wired | What goes wrong | |---|---|---| | Inside each handler | Handler catches or inspects the violations and builds a body | Every endpoint invents its own field names, codes and casing; new endpoints forget entirely | | In a shared helper | Handlers call one function, by convention | Works until someone does not call it; the convention is invisible to the framework and to review | | On the framework's error hook | One registered component intercepts the validation failure before any handler body runs | The shape is enforced by the framework rather than by discipline | Frameworks differ in what the hook is called and in how it is registered — some resolve a handler by the failure type, others run an outer wrapper around the whole chain — but they generally offer a place to say "when validation fails anywhere, run this". That is the seam this pattern uses. ## What the central converter does 1. **Receives** the violation set, plus enough context to know which model it came from. 2. **Translates paths** from model property names into the names the body actually uses on the wire. 3. **Resolves messages** — a key and its parameters become text, or are passed through as a stable code with parameters so the client can render its own wording. 4. **Sorts** deterministically, usually by path, so responses and tests do not depend on enumeration order. 5. **Applies a redaction policy** to rejected values, per field or per rule. 6. **Hands the result to whatever writes the service's failure body**, so the envelope stays the same as for every other failure the service produces. ## Why per-handler shaping drifts - Twelve handlers produce twelve opinions: `field` vs `name` vs `property`, `NOT_BLANK` vs `required`, camel case in one place and snake case in the next. - A thirteenth endpoint is added under time pressure and returns a bare sentence, so the client's field-highlighting code silently does nothing for it. - Renaming one key in the body becomes a twelve-file change with no compiler help. - Each handler needs its own error-path test, so the suite grows with endpoints instead of with rules. - Cross-cutting policy — redaction, a correlation identifier, a documentation link — has to be remembered twelve times. ## The costs, honestly A central converter must be **general**. Endpoint-specific wording should not live inside it, or it grows a switch over routes and becomes the thing it replaced. The escape hatch is to push the specificity back to where the rule was declared: a constraint carries its own message key, and the converter renders whatever key it is given. Rule groups let the same model carry different rule sets on different endpoints without the converter knowing anything about routes. A second cost is **distance**. The handler author no longer sees the response their endpoint produces for bad input, so the converter needs its own tests and the body needs documenting where handler authors will read it. A contract test that posts a deliberately bad body to several unrelated endpoints and asserts the same envelope shape is the cheap guard: it fails the moment someone hand-rolls a body in a handler. ## When one endpoint genuinely needs something different It happens — a legacy integration, a form that must return a flat list of sentences. Keep it inside the one converter as an explicit branch keyed on something durable (a marker on the model, a response type), not by letting that handler build its own body. One component with a documented exception is still one contract; two components are two contracts, and the second one starts drifting the day it is written. ## What good answers say That the violation set is an internal representation and the error body is a published shape; that the framework offers exactly one place where the mapping between them belongs; and that the payoff is not tidiness but **change cost** — the day the body has to gain a documentation link or a request identifier, that is one edit and one test, applied uniformly to endpoints nobody has to enumerate.

  • How does an endpoint get different wording if the converter is generic?
    Through the constraint, not the converter. Each declared rule carries its own message key, and rule groups let one model apply different rule sets on different endpoints. The converter renders whatever key it is handed, so specificity lives next to the rule while the body shape stays uniform.
  • What test proves the central converter is actually being used?
    A contract test that posts a deliberately invalid body to several unrelated endpoints and asserts one identical envelope — same keys, same casing, same code vocabulary. It fails the moment someone hand-rolls a body in a handler, which no per-handler unit test would catch.
  • One endpoint must return a flat list of sentences for a legacy client. How do you handle it?
    Keep it inside the one converter as an explicit branch keyed on something durable, such as a marker on the model or the response type. One component with a documented exception is still one contract; a second converter becomes a second contract that drifts immediately.

saying these in an interview costs you the question

  • Believes each handler should format its own validation errors for flexibility
  • Thinks a shared helper handlers must remember to call is equivalent
  • Puts route-specific wording inside the converter as a branch over paths
  • Assumes the converter needs no tests because handlers have some
  • Cannot name where in the framework such a component is registered