Tutorials

How to Migrate from Contentful to Decoupled.io

Jay Callicott··8 min read

How to Migrate from Contentful to Decoupled.io

Contentful is a solid headless CMS, but as your project grows, the cracks start to show. Pricing jumps from free to $300/month with no middle ground. API call limits force you to think about caching before you've even launched. And your content is locked in a proprietary format that makes switching painful — which is, of course, the point.

If you've been weighing alternatives, here's a practical guide to moving from Contentful to Decoupled.io without losing your content, your content model, or your mind.

Why Migrate

Pricing that scales against you. Contentful's free tier caps you at 10K records and 100K API calls per month. The next tier is $300/month — there's nothing in between. Enterprise pricing is opaque and often runs $5K-$70K/year. With Decoupled.io, pricing is transparent and based on usage, not seat counts or record limits.

Vendor lock-in. Your content lives in Contentful's cloud in a Contentful-specific format. You can export it, but the export is not portable. Your content model, your content, and your editorial workflows are all tied to their platform.

SDK dependency. Contentful's JavaScript SDK is the primary way to query content. It works, but it's another proprietary layer between your code and your data. Decoupled.io's typed client gives you the same developer experience — autocomplete, type safety — but built on standard GraphQL under the hood.

What You'll Need

  • A Decoupled.io account
  • Your Contentful space export (via the Contentful CLI or Management API)
  • Access to your frontend codebase
  • About 2-4 hours for a typical migration

Step-by-Step Migration Plan

Step 1: Export Your Contentful Content Model and Content

Use the Contentful CLI to export your space:

contentful space export --space-id YOUR_SPACE_ID --management-token YOUR_TOKEN

This gives you a JSON file containing your content types, entries, and assets. Review the export to understand which content types and fields you're working with.

Step 2: Recreate Your Content Model in Drupal

In the Decoupled.io dashboard, create content types that map to your Contentful content types. The concepts translate directly:

Contentful Decoupled.io (Drupal)
Content Type Content Type
Entry Node
Asset Media
Rich Text Paragraphs / Rich Text
Reference Entity Reference
Locale Translation

Most content models map one-to-one. Contentful's "Rich Text" field becomes either a formatted text field or a Paragraphs field in Drupal, depending on how much structure you need.

Step 3: Migrate Your Content

Use the Drupal Migrate API to import your exported content. Decoupled.io provides migration tooling that reads Contentful's export format and maps it to your new content types. For each content type, you'll define a field mapping — which Contentful field goes to which Drupal field.

Assets (images, files) are migrated as Drupal media entities with full CDN support.

Step 4: Generate Your Typed Client

Once your content is in Decoupled.io, generate the typed client for your frontend:

npx decoupled-cli@latest schema sync

This introspects your Drupal schema and generates TypeScript interfaces and queries — the same developer experience you had with Contentful's SDK, but without the vendor lock-in.

Step 5: Update Your Frontend

Swap out Contentful SDK calls for typed client calls. The patterns are similar, so this is mostly a find-and-replace operation.

What Changes in Your Frontend Code

The migration from Contentful's SDK to decoupled-client is straightforward. Here's a typical before and after:

Before (Contentful SDK):

import { createClient } from 'contentful'

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
})

const entries = await client.getEntries({
  content_type: 'blogPost',
  limit: 10,
})

const title = entries.items[0].fields.title

After (decoupled-client):

import { createClient } from 'decoupled-client'
import { createTypedClient } from './schema/client'

const base = createClient({
  baseUrl: process.env.NEXT_PUBLIC_DRUPAL_BASE_URL!,
  clientId: process.env.DRUPAL_CLIENT_ID!,
  clientSecret: process.env.DRUPAL_CLIENT_SECRET!,
})

const client = createTypedClient(base)

const articles = await client.getEntries('NodeArticle', { first: 10 })

const title = articles[0].title

The key difference: with the typed client, articles[0].title is fully typed. Your IDE knows exactly what fields exist on each content type because they're generated from your actual schema.

What Stays the Same

The good news is that the core concepts don't change:

  • Content modeling — You still define content types with fields. Drupal's entity-field system is more powerful than Contentful's, but the mental model is the same.
  • Editorial workflow — Content editors still log into an admin panel, create and edit content, and publish when ready. Drupal's editorial workflow is more mature, with built-in content moderation, revision history, and role-based access.
  • API-first architecture — Your frontend still fetches content via API. The URL changes, the SDK changes, but the pattern is identical.
  • Media handling — Images and files are still managed as assets with automatic optimization and CDN delivery.

Next Steps

  1. Get started with Decoupled.io — Set up your account and create your first content type.
  2. See how Decoupled.io compares to Contentful — A detailed feature-by-feature comparison.
  3. Explore pricing — Transparent pricing without per-seat or per-record surprises.
  4. Learn the typed client — Full documentation for decoupled-client, including queries, filtering, and pagination.