A Practical Headless Approach Using OneEntry
In most modern projects, frontend and backend are already architecturally separated. We use headless CMS platforms, API-first approaches, SDKs, and frameworks such as Next.js or Nuxt, and this architecture is expected to simplify product development. In practice, however, one persistent problem remains.
Almost any change to the data structure still leads to frontend updates, code changes, and a new deployment. In many cases, these are not changes to business logic, but simple content-related updates: adding a new product attribute, renaming a field, or displaying an additional parameter on a page. From a business perspective, these are minor adjustments. From a development perspective, they become a full-fledged task that affects frontend code and the delivery process.
In this article, we will examine why this tight coupling between data and frontend remains typical for most headless solutions, how alternative architectural approaches are usually implemented, and why OneEntry allows data to be managed without any changes to frontend code.
A Typical Scenario: Data Is Tightly Coupled to the Frontend
Let’s consider a common scenario. A project already has a product page, a working frontend, and a properly implemented API integration. At some point, a business request comes in: add a material field, introduce a “Delivery time” attribute, or display a few additional parameters for a category.
From a business perspective, these are purely content changes. No new logic is required, user flows remain the same, and the system’s behavior does not change. In practice, however, in most solutions such updates require changes to data schemas, type definitions, and frontend components, followed by a new deployment.
As a result, the frontend becomes a bottleneck even for simple changes that do not affect business logic.
Why This Happens in Most Headless CMS Platforms
If we look at the problem more closely, the reason becomes clear. Historically, headless CMS platforms were designed around strict data models. Content types are defined upfront, schemas are fixed, and fields are explicitly tied to how they are used in the frontend.
In this kind of architecture, the frontend knows the data structure in advance, directly consumes specific fields in components, and relies on static types or predefined GraphQL queries. As long as the data structure remains unchanged, this approach is convenient and predictable. However, once requirements begin to evolve, the tight coupling between data and interface quickly becomes a limitation.
Instead of simplifying product evolution, the architecture starts to resist change and increasingly requires frontend developer involvement even in cases where only the data itself needs to be updated.
The OneEntry Approach: Data First, Code Second
From the outset, OneEntry is built on a different architectural principle: the frontend should not need to know in advance which attributes exist for a given entity. Instead of rigid, predefined schemas, OneEntry uses a unified attribute model in which the data structure can evolve independently of frontend code.
Attributes are created and configured in the administrative interface and then assigned to the relevant entities—products, pages, users, orders, and other system objects. The API and SDK automatically return all associated attributes, while the frontend works with a universal data structure that does not depend on a fixed set of fields.
Adding a new attribute changes the data but does not require any modifications to the frontend application code.
In Practice: Adding a Product Attribute
Let’s walk through this approach using a practical example.
- In the first step, a new attribute is created in the OneEntry administrative panel. The attribute type can be anything—string, number, boolean, list of values, or file.
- Once created, the attribute is assigned to the Product entity and saved. At this stage, no frontend developer involvement is required.
- Immediately after saving, the attribute becomes available in both the API and the SDK. No additional configuration or query changes are needed, as the OneEntry API and JavaScript SDK automatically start returning the new attribute together with the product data.
- At the next stage, the frontend receives the updated data without any code changes. Let’s see how this looks in a Next.js application.
Example: Fetching a Product in Next.js Using the OneEntry SDK
Installing the SDK
npm install oneentry
SDK Configuration
// lib/oneentry.ts
import { defineOneEntry } from "oneentry";
export const oneentryApi = defineOneEntry(
"https://<your-project>.oneentry.cloud",
{
token: process.env.ONEENTRY_API_TOKEN,
langCode: "en_US",
}
);
Product Page Using the App Router
// app/products/[slug]/page.tsx
import { oneentryApi } from "@/lib/oneentry";
interface ProductPageProps {
params: { slug: string };
}
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = params;
const response = await oneentryApi.Products.getProductsByPageUrl(
null,
"en_US",
{
pageUrls: [slug],
limit: 1,
offset: 0,
}
);
const product = response.items[0];
return (
<main>
<h1>{product.title}</h1>
<ul>
{product.attributeValues.map((attr) => (
<li key={attr.id}>
<strong>{attr.title}:</strong> {String(attr.value)}
</li>
))}
</ul>
</main>
);
}
What’s Important
- In this example, the frontend does not know in advance which attributes exist for a product. It simply processes the
attributeValuesarray returned by the API. - A new attribute appears automatically, without any changes to components and without a redeployment.
- Each attribute has its own identifier.
At this point, the key architectural advantage becomes clear. If the frontend application is designed around a universal data structure from the beginning, attribute identifiers can be used as control signals for the UI. In this model, attributes become part of a declarative UI definition.
Through conventions and rules implemented on the frontend side, it is possible to define where and how specific attributes should be displayed: in the product card, in a technical section, inside tabs or accordions, or in auxiliary sections of the page.
As a result, when a new attribute is added in the administrative panel, its role in the interface can be defined immediately. The frontend code remains unchanged, and no deployment is required.
It is important to note that OneEntry does not enforce a specific model for working with attributes. Different approaches are possible, including naming conventions, grouping by type, the use of dedicated flag attributes, or custom rules defined by the team. The platform provides the data and metadata, while the decision on how to interpret and render them in the UI remains with the developers.
Dynamic UI Management Through Attributes
Because each attribute has a unique identifier, it can be used not only as a key for rendering a value, but also as part of the interface logic. In this approach, attributes act simultaneously as data and as instructions for the UI.
The simplest and most commonly used option is a prefix-based convention. For example, a team can agree that attributes with the card_ prefix are displayed in the main product card, tech_ attributes appear in a technical section, tab_ attributes are rendered inside tabs, and meta_ attributes are used for SEO or internal purposes.
An administrator adds attributes with the appropriate identifiers, such as card_material, tech_weight, or tab_delivery_time. The frontend remains unchanged.
On the frontend side, the logic remains simple and transparent:
const cardAttributes = product.attributeValues.filter(attr =>
attr.identifier.startsWith("card_")
);
const techAttributes = product.attributeValues.filter(attr =>
attr.identifier.startsWith("tech_")
);
const tabAttributes = product.attributeValues.filter(attr =>
attr.identifier.startsWith("tab_")
);
This is followed by standard component rendering:
<section>
<h2>Key Specifications</h2>
<ul>
{cardAttributes.map(attr => (
<li key={attr.id}>
<strong>{attr.title}:</strong> {String(attr.value)}
</li>
))}
</ul>
</section>
When a new attribute is added with the appropriate prefix, it is automatically rendered in the correct part of the interface. No code changes or redeployments are required.
Prefixes Are Not the Only Option
Using prefixes is just one possible way to organize rendering logic. The OneEntry architecture does not restrict frontend teams to this approach and does not enforce any specific rules. In practice, teams use different patterns, including grouping by identifiers, naming conventions, dedicated flag attributes, configuration attributes for UI control, or combinations of multiple rules.
The key architectural principle of OneEntry is a clear separation of responsibilities. The logic for interpreting data and rendering the interface lives on the frontend side, while the data itself and its structure are managed in the administrative panel. This allows content and interface to evolve independently of each other.
Why This Matters
In this model, the content team manages data directly without involving developers. The frontend remains stable, while the interface stays extensible. The system does not dictate how the UI should be structured, but instead provides the tools needed to build a custom architecture.
OneEntry does not impose rigid rendering rules. The platform delivers all necessary data and metadata, while decisions about how to translate them into an interface are made at the level of the project’s frontend architecture.
Why This Works
The approach is based on treating attributes as independent entities. The API and SDK return a stable data structure that includes both attribute values and metadata. This allows the frontend application to work with a data model rather than a fixed schema, and to remain independent of changes in content structure.
What This Means for Business
Changes can be introduced faster, the number of development tasks is reduced, content teams can work independently, and the risks associated with unnecessary deployments and regressions are significantly lower.
What This Means for Developers
Frontend code becomes more stable, the stream of small “just update a field” tasks disappears, components become more reusable, and project maintenance and scaling are simplified.
When This Approach Is Especially Effective
This approach works particularly well for projects with large catalogs, marketplaces, corporate websites, MVPs and pilot projects, as well as systems where data and interface requirements change frequently.
Conclusion
In closing, it is important to emphasize one key idea: the headless approach is not just about separating frontend and backend. Its core purpose is to separate code from the evolution of data. If any content change still requires frontend modifications, it is a clear sign that the architecture remains too tightly coupled.
OneEntry was designed from the ground up to allow data to evolve freely without requiring changes to frontend code.
We hope this article was useful to you, and we would be glad to see you among OneEntry users.
Top comments (2)
Hi Kai Alder,
We haven’t run into performance issues caused by the attribute model itself, as long as it’s treated like a normal data-to-UI layer and you’re not doing heavy work directly inside the render phase.
In practice, the bottleneck usually isn’t the attributes array itself, but what you do with it:
-running .filter() or .sort() on every render
-building derived structures like tabs, groups, or sections without memoization
-rendering long lists without virtualization
In React, useMemo is absolutely reasonable for derived lists, especially if you have a lot of attributes or the component re-renders frequently.
Another practical pattern is converting attributeValues into a lookup object keyed by identifier once, so you’re not scanning the array repeatedly.
So yes, your instinct is right. In some places useMemo definitely makes sense. We just try to keep the mapping layer simple and only memoize where it actually brings measurable benefit.
Have you noticed any performance issues with this approach at scale? I tried something similar and had to add useMemo in a couple spots. Curious about your experience.