pattern

Response Shape Allowlist

also called Named View Projection, Registered Response Shape

Restricting an API to a small named set of response shapes per resource instead of arbitrary field selection, so responses stay cacheable, server work stays bounded and clients cannot invent an expensive query in production.

api-designcachinggraphqlpersisted-queriesover-fetching

A team adds ?fields= to a list endpoint to cut mobile payloads. Payload sizes fall about 40% and everyone is pleased. Two weeks later the CDN hit ratio on that endpoint has gone from 82% to 11%, the database is doing more work than before the change, and nobody can say which field combinations are in use.

The clients did exactly what the parameter invited: each screen asked for what it needed, and there are now several dozen distinct shapes in production. Payload size fell and total cost rose, because the saving was in bytes and the loss was in cache keys.

Why it matters

A cache key is the URL plus the headers it varies on. Every distinct field combination is a distinct key, so n optional fields admit up to 2^n responses — ten optional fields is 1,024 possible bodies — and a few dozen in real use is enough to shred a cache whose hit ratio depended on a handful of keys. A hit ratio falling from 82% to 11% multiplies origin requests by roughly five.

The second cost is on the server and worse because it is invisible. For a shape it has never seen, the server cannot precompute, cannot batch, cannot index deliberately, and cannot tell which fields are load-bearing when it wants to change one. An API accepting arbitrary shapes has no enumerable surface, so it cannot be performance-tested or safely evolved.

Implementation patterns

  • Name shapes after screens: view=product-card, view=product-detail. Three to a dozen per resource is the working range.
  • Make each view a server artefact with a fixed projection and join set, its own cache lifetime and latency budget, and a test asserting the query plan.
  • Add views by pull request. Slower than a query parameter, and that is the point: shapes become countable, reviewable and deletable.
  • For a graph query API the equivalent is registered documents: the client sends an identifier, the server holds the document text, arbitrary queries are rejected in production. Standard practice for publicly exposed graph APIs since roughly 2017.
  • Count your Vary headers too. Vary: Accept-Encoding, X-Device-Class, X-Experiment multiplies the key space exactly as a field parameter does, and it is easier to add by accident.

Industry example

The pattern is most visible wherever a graph query interface is exposed outside the organisation that wrote the clients. Public graph APIs restrict callers to pre-registered documents, because an arbitrary query is an unbounded authorisation to spend the server's time and no rate limit in requests per second can bound it.

The archetype in the other direction is an image-heavy discovery feed serving mobile clients: dozens of screens each wanting a slightly different projection of the same item, with a CDN in front doing most of the work. The feed survives because its shapes are few and named, not because its payloads are small.

Failure scenarios

  • Cache shredding, as above: payloads shrink, hit ratio collapses, origin load rises.
  • A silent cost change. A client adds one field to a view, that field needs an extra join, and the view's latency doubles with no server review.

Trade-offs

Choose Gains Pays
Named shapes cacheable responses, testable query plans, enumerable surface, bounded cost per call a server change per new screen variant, and a registry to keep tidy
Arbitrary field selection clients iterate without server work, minimal payloads per screen cache key explosion, untestable cost, no safe way to evolve a field

The tension is velocity: arbitrary selection lets a client team ship a variant in an afternoon, and the allowlist moves that cost onto a server team. The trade is worth it precisely when a shared cache or database makes one client's convenience everyone else's bill.

When not to use it

An internal API with one consumer, no shared cache and no shared database contention: arbitrary field selection is cheaper than a registry and the blast radius is one team.

The decision flips when any of three things becomes true: a cache sits in front, the consumer count passes a handful, or the API becomes public. Adding the allowlist after a cache is in place means discovering which shapes exist, which is archaeology rather than design.

Interview question

Q: "A mobile team asks for a sparse-fieldset parameter on your highest-traffic list endpoint to cut payload sizes by 40%. The endpoint sits behind a CDN with an 82% hit ratio. What do you say, what do you offer instead, and what measurement would change your mind?"

What a strong answer covers: cache key cardinality named as the cost rather than an argument about bytes; three named views offered that cover the team's screens; a check of whether payload size or main-thread render cost is the client's real constraint; and the measurement that flips the answer, a hit ratio already low enough that nothing is being protected.

Quick check

Quiz: You add a fields= parameter and payloads fall 40% while origin load rises. Why? — Every field combination is a separate cache key, so the hit ratio collapses and the origin serves what the CDN absorbed.

Flashcard: What replaces arbitrary field selection without reinstating over-fetching? — A small named set of response shapes per resource, each a server-side artefact with its own cache lifetime and latency budget; for graph APIs the equivalent is registered documents.