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 > ); }; It works. Until you migrate to Strapi. Now the response might look completely different: product . title product . description product . image . url 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 │ └───────────────┘ 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 ; }; }; 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 ?? "" , }, }; } Now the component doesn't care about Strapi: < ProductCard product = { product } /> 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 ↓ ????? Without an abstraction layer: CMS ↓ API ↓ React components ↓ Business logic 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 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 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 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 } } } 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 or: Strapi → Shopify or: WordPress → Contentful 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