DEV Community

Cover image for Migrating a Headless CMS? Your Frontend Shouldn't Know About It
Leonel Oliveira
Leonel Oliveira

Posted on

Migrating a Headless CMS? Your Frontend Shouldn't Know About It

A headless CMS migration often sounds simple:

Contentful → Strapi

Move the content, update the API calls, fix a few components, and you're done.

Except... you're usually not.

The hardest part of a headless CMS migration isn't moving the content.

It's managing the contract between the CMS and the frontend.

And if your React or Next.js application is tightly coupled to the CMS response structure, changing the CMS can turn into a much bigger project than expected.

The problem

Imagine your frontend directly consumes Contentful responses:

const ProductCard = ({ product }) => {
  return (
    <article>
      <h2>{product.fields.title}</h2>
      <p>{product.fields.description}</p>
      <img src={product.fields.image.fields.file.url} />
    </article>
  );
};
Enter fullscreen mode Exit fullscreen mode

It works.

Until you migrate to Strapi.

Now the response might look completely different:

product.title
product.description
product.image.url
Enter fullscreen mode Exit fullscreen mode

Suddenly, the frontend needs to understand both CMS structures.

And this problem isn't limited to simple fields.

Things become much more complicated with:

  • Rich text
  • Media and assets
  • References
  • Nested relations
  • Localization
  • Draft/preview content
  • SEO metadata
  • Dynamic components
  • Pagination
  • GraphQL vs REST
  • Different content modeling approaches

The architecture I prefer

Instead of allowing React components to consume the CMS directly, introduce a layer between the CMS and the application.

                ┌───────────────┐
                │    Strapi     │
                └───────┬───────┘
                        │
                        ▼
                ┌───────────────┐
                │ CMS Adapter   │
                └───────┬───────┘
                        │
                        ▼
                ┌───────────────┐
                │ Domain Model  │
                └───────┬───────┘
                        │
                        ▼
                ┌───────────────┐
                │ React / Next  │
                └───────────────┘
Enter fullscreen mode Exit fullscreen mode

The frontend doesn't need to know whether the data came from Strapi, Contentful, Shopify, WordPress, or something else.

It just receives the data it needs.

For example:

type Product = {
  id: string;
  title: string;
  description: string;
  image: {
    url: string;
    alt: string;
  };
};
Enter fullscreen mode Exit fullscreen mode

The CMS adapter is responsible for transforming the CMS response into this model.

function mapStrapiProduct(data: StrapiProduct): Product {
  return {
    id: data.id,
    title: data.title,
    description: data.description,
    image: {
      url: data.image.url,
      alt: data.image.alternativeText ?? "",
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Now the component doesn't care about Strapi:

<ProductCard product={product} />
Enter fullscreen mode Exit fullscreen mode

That's a much healthier boundary.

Why this matters during a migration

Let's say six months later the company decides to move again.

Contentful
     ↓
Strapi
     ↓
?????
Enter fullscreen mode Exit fullscreen mode

Without an abstraction layer:

CMS
 ↓
API
 ↓
React components
 ↓
Business logic
Enter fullscreen mode Exit fullscreen mode

You potentially have CMS-specific assumptions spread throughout the application.

With an adapter:

             ┌── Contentful Adapter
             │
CMS ─────────┼── Strapi Adapter
             │
             └── Shopify Adapter
                     ↓
                Domain Model
                     ↓
                React / Next.js
Enter fullscreen mode Exit fullscreen mode

The frontend remains largely unchanged.

Only the data integration layer needs to change.

But there is another challenge: content migration

Changing the API isn't enough.

You also need to think about how existing content maps to the new content model.

For example, a rich text field in Contentful might not map directly to the structure expected by Strapi.

The same applies to relationships.

You might have:

Contentful
Article
 ├── Author
 ├── Categories
 ├── Related Articles
 └── Hero Image
Enter fullscreen mode Exit fullscreen mode

While your Strapi model might represent those relationships differently.

So the migration becomes a mapping problem:

Contentful Model
       ↓
Migration / Transformation
       ↓
Strapi Model
       ↓
Frontend Domain Model
Enter fullscreen mode Exit fullscreen mode

This is why I don't think of a CMS migration as simply:

"Move the content from A to B."

I think of it as:

"Preserve the application's content contract while changing the underlying content platform."

What about GraphQL?

GraphQL can make this architecture even more interesting.

A frontend might consume:

query Product($id: ID!) {
  product(id: $id) {
    title
    description
    image {
      url
      alt
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

But the important question isn't necessarily whether the API is GraphQL or REST.

The important question is:

Where does the responsibility for transforming external data live?

If every React component understands the CMS schema, the coupling remains.

If the API layer transforms external data into application-specific models, the frontend becomes much more independent.

The rule I try to follow

I like this simple rule:

Your UI should depend on your application's data model, not your CMS's data model.

The CMS is an implementation detail.

Your frontend shouldn't care whether:

Contentful → Strapi
Enter fullscreen mode Exit fullscreen mode

or:

Strapi → Shopify
Enter fullscreen mode Exit fullscreen mode

or:

WordPress → Contentful
Enter fullscreen mode Exit fullscreen mode

happens six months from now.

Ideally, the React components shouldn't need to know.

When I wouldn't use this approach

There is a trade-off.

For a small application with:

  • 5 pages
  • 3 content types
  • 1 developer
  • no plans to change the CMS

Adding several abstraction layers might be unnecessary complexity.

Architecture should solve a problem, not create one.

But for a large application with hundreds of content types, multiple teams, localization, preview environments, and thousands of entries?

The separation becomes much more valuable.

Final thoughts

A headless CMS gives us a lot of flexibility.

But it's easy to accidentally recreate the same coupling we were trying to eliminate.

The CMS may be headless, but that doesn't automatically mean the frontend is decoupled.

For me, the interesting part of a CMS migration isn't:

"How do we move the content?"

It's:

"How do we change the content platform without forcing the entire frontend to change with it?"

That's where good architecture can make a huge difference.


What do you think?

When working with a headless CMS, do you prefer:

A. Keeping the frontend tightly aligned with the CMS schema because it's simpler?

B. Creating an abstraction/domain layer so the frontend remains CMS-agnostic?

I'd be interested to hear how other teams approach this.

webdevelopment #frontend #react #nextjs #typescript #headlesscms #strapi #contentful #softwarearchitecture #javascript

Top comments (0)