The API I wish every CMS had
Content modeling as if the frontend mattered. A look at the shape of a CMS that respects the people building on it.
Most CMS APIs are designed for the database, not for the person rendering the page. You ask for an article and you get back a row: a flat bag of fields, some of which are HTML strings, some of which are foreign keys you now have to resolve with three more requests. The shape of the response mirrors the shape of the storage. That choice quietly pushes a pile of work onto every frontend that touches it, and it never stops paying for itself.
The CMS I want inverts that. It treats the API response as a contract with the rendering layer, not a dump of the table. Content modeling becomes a frontend concern that happens to be stored somewhere, rather than a storage concern the frontend has to clean up after. Everything below follows from taking that one idea seriously.
Give me a tree, not a soup of HTML
Rich text is where most CMS APIs give up. They hand you a blob of HTML and wish you luck. Now every consumer is parsing markup with a regex or a fragile DOM walk, hoping the editor didn't paste a style attribute from Word, hoping an embedded video didn't arrive as an opaque iframe you can't restyle for mobile.
A respectful API gives me rich text as a structured tree of typed nodes. A paragraph is a node. A heading carries its level as data. An embedded image is a node with an asset reference, dimensions, and a focal point — not an img tag with a CDN URL baked in. When I render, I map node types to my own components. The CMS never decides what a heading looks like, because that was never its job.
The moment a CMS emits HTML, it has made a design decision on a frontend it will never see.
This is the difference between a CMS that ships a <blockquote> and one that ships a node that says "this is a quote, here is its text, here is its attribution." The first locks me into someone else's markup. The second lets me render a pull quote, a card, or a tweet-shaped thing, and change my mind next quarter without re-editing a single document.
Resolve references for me, but let me choose the depth
The second tax is the reference resolution waterfall. An article references an author, the author references an avatar, the article references three related posts that each reference their own authors. With a naive API that is one request per hop, and a loading spinner that grows with the content graph.
I want references resolved server-side, in one round trip, with the depth under my control. Let me say how far to follow the graph and which fields to pull at each level, the way a good GraphQL schema or a projection query does. The shape of what comes back should match the shape of what I render — nested where my UI is nested, flat where it's flat.
- →Let me request only the fields a given view needs, so a list page doesn't pay for body content it will never show.
- →Resolve assets to real metadata: dimensions, format, a dominant color for the placeholder, an optional blur token.
- →Keep unresolved references as stable IDs, not nulls, so a missing related post degrades into a skip instead of a crash.
The test is simple. If rendering a page takes more than one request to the content API, the API made its convenience my problem.
Model variants and states, not just the happy path
Real content has states the database schema usually pretends don't exist. A piece is published, or it's a draft, or it's scheduled, or it's a version someone is previewing before it goes live. Most APIs expose the published row and bolt preview on as a separate, differently-shaped endpoint you discover the hard way.
I want one query shape that takes a perspective — published, draft, a specific release — and returns the same typed structure for all of them. Preview is not a different API. It's the same API with a different lens. That single decision collapses an entire category of "works in prod, breaks in preview" bugs, because the preview path and the production path run the exact same rendering code.
The same goes for localization and personalization variants. A field that varies by locale should come back as the resolved value for the requested locale, with a clear, typed signal when a translation is missing and it fell back — not a silent return of the default language that nobody notices until a customer screenshots it.
Make the types real
The last thing I want is a schema I can generate types from, and a guarantee the API honors them. If a field is required in the model, it is never null in the response. If a reference is typed as an author, it never resolves to a landing page. This sounds obvious and it is routinely violated, because the storage layer allows states the content model swears are impossible.
The type is the contract — every node maps to a component I own.
When the types are honest, rendering becomes a lookup. Every node has a type, every type maps to a component, and an unknown type fails into nothing instead of a thrown error in production. The frontend stops being a defensive cleanup layer and becomes what it should have been: a thin, total mapping from content to components. The bugs that used to live in the gap between "what the schema promised" and "what the API actually sent" simply have nowhere to live.
None of this requires a new category of product. It requires a CMS that decides, on purpose, that the people building on it are the customer — and that the cost of a sloppy response shape is real, paid daily, by everyone downstream. Most don't. The one I want would, and it would win on that alone.
