Treat Your Headless CMS Like a Hostile External API
One of the better architectural decisions we made this year was refusing to let any UI component touch a raw CMS response. A thin mapping layer turns the vendor payload into the application's own vocabulary, contains change, and validates at the boundary.
Treat your headless CMS like a hostile external API.
It sounds harsh, but it was one of the better architectural decisions we made on a large enterprise content platform this year.
The rule is simple. No UI component ever touches a raw CMS response.
Between the CMS and the UI sits a thin translation layer with one job. Turn the vendor’s payload into the application’s own vocabulary.
Data flows in one direction: CMS (GraphQL) → client → mapper → typed props → component
The types are deliberately different at each boundary.
- Raw CMS shapes are named Aem*
- UI-ready props are named Mapped*
That distinction matters. A component cannot accidentally accept a raw CMS payload without TypeScript complaining about it. The architecture is not just a diagram sitting in a wiki somewhere. The type system helps enforce it.
A mapper should be small and boring. That’s exactly the point.
AemTitleAndBody comes in. The mapper converts rich text into safe HTML, validates required fields, and if the body is missing, fails immediately with the content path. MappedTitleAndBody comes out.
You get several things from this pattern.
-
CMS changes stay contained. When the CMS response changes, and eventually it will, you update the mapper instead of chasing field names through dozens of components.
-
Validation happens at the boundary. Malformed or missing content fails where it enters the application, with useful context, instead of surfacing three layers later as some meaningless undefined error during rendering.
-
Components stay presentational. They can be tested with plain fixtures and rendered in Storybook without knowing anything about AEM, GraphQL, or the CMS.
-
The UI is decoupled from the CMS. Changing the content source is still real work, but the blast radius is dramatically smaller because CMS-specific assumptions are concentrated at the boundary instead of distributed throughout the application.
The mental model that made this click for the team was simple.
The mapper is the last place in the application allowed to know the CMS exists.
Everything downstream speaks the language of the application, not the language of the vendor.
If you’re building headless, put this anti-corruption layer in place on day one. Retrofitting it after your component model already speaks in CMS field names is the expensive version.