---
title: "API content modeling: structuring models that scale"
description: "Good API content modeling means typed content types, references
instead of duplication, reusable components, and shallow nesting. Model
relationships explicitly and avoid the three classic traps:..."
url: "https://adaptocms.com/articles/api-content-modeling-guide/"
category: "Tutorials"
date: "2026-07-01"
source: adaptocms.com
---

# API content modeling: structuring models that scale

## TL;DR

Good API content modeling means typed content types, references instead of duplication, reusable components, and shallow nesting. Model relationships explicitly and avoid the three classic traps: god-types, per-locale duplication, and over-nesting.

API content modeling is designing the shape of your content before you write any of it: which content types exist, what fields each one has, how types reference each other, and which field groups get reused as components. Get this wrong in a headless CMS and the mistake spreads: it shows up in every frontend query, every editor screen, and eventually in a migration project nobody budgeted for.

Most content-modeling guides stop at that definition and stay abstract: "think about your content types," "plan your relationships." That's not enough to build one. This guide is example-driven: real (illustrative) JSON payloads for types, fields, references, and components, the concrete pitfalls that turn a clean model into a mess, and what a locale-aware model looks like before you need it.

## The building blocks: types, fields, references, components

Four concepts cover almost every content model you'll build.

**Content type (or collection).** A schema for a kind of content: Article, Page, Author, Product. Each entry of a type shares the same fields. This is the unit you fetch, cache, and version.

**Fields.** The typed attributes on a type: `text`, `richText`, `number`, `boolean`, `date`, `media`, plus the two structural field types below. A field's type determines its validation and how the API serializes it. Get the type wrong (e.g. a `text` field for something that should be structured) and you'll be regex-parsing strings in your frontend later.

**Reference fields.** A pointer from one entry to another entry that has its own identity and its own edit screen. An Article referencing an Author is a reference: the Author exists independently, gets edited on its own, and is reused across many articles. References are how you avoid duplicating data; they're also how "fetch one article" can turn into three round trips if you're not careful about resolving depth (this is one of the concrete tradeoffs between REST and GraphQL; see [REST vs GraphQL for a headless CMS](/articles/rest-vs-graphql-headless-cms/) for the fetching-depth argument in full).

**Components.** A reusable group of fields with no independent identity: an SEO block, a CTA block, an address block. A component isn't a type: it doesn't get its own edit screen or its own API endpoint, it's a repeated shape you embed inside a type. The test is simple: if the thing needs its own lifecycle and gets reused *across entries*, make it a type and reference it. If it's a repeated *shape of fields*, make it a component and embed it.

## A concrete example: modeling a blog

This is a real schema rather than a diagram. The JSON below is illustrative, to show the shape of the decisions. Check your CMS's own schema syntax before copying it literally.

An `Author` type (simple, no references out):

```
{
  "type": "author",
  "fields": [
    { "name": "name", "type": "text", "required": true },
    { "name": "bio", "type": "richText" },
    { "name": "avatar", "type": "media" }
  ]
}
```

A reusable `seo` component (no identity of its own, only a field group):

```
{
  "component": "seo",
  "fields": [
    { "name": "meta_title", "type": "text" },
    { "name": "meta_description", "type": "text" },
    { "name": "og_image", "type": "media" }
  ]
}
```

An `Article` type that references `Author`, references `Category` (many-to-many), and embeds the `seo` component instead of redefining those three fields inline:

```
{
  "type": "article",
  "fields": [
    { "name": "title", "type": "text", "required": true },
    { "name": "slug", "type": "text", "required": true, "unique": true },
    { "name": "body", "type": "richText" },
    { "name": "author", "type": "reference", "target": "author", "cardinality": "one" },
    { "name": "categories", "type": "reference", "target": "category", "cardinality": "many" },
    { "name": "seo", "type": "component", "target": "seo" },
    { "name": "published_at", "type": "date" }
  ]
}
```

And an entry created against that model. This is the payload shape your frontend or CMS client would send or receive, generic JSON again rather than a literal API contract:

```
{
  "title": "API content modeling: structuring models that scale",
  "slug": "api-content-modeling-guide",
  "author": { "ref": "author_84f2" },
  "categories": [{ "ref": "category_tutorials" }],
  "seo": {
    "meta_title": "API Content Modeling: Structuring Models That Scale",
    "meta_description": "Structure content models with concrete JSON examples."
  },
  "published_at": "2026-07-01"
}
```

Three types (`author`, `category`, `article`), one component (`seo`), zero duplicated fields. That's the whole trick. Everything after this is applying it consistently.

Worth being precise about what's illustrative and what isn't here. The field-type names above (`text`, `richText`, `reference`, `component`) show the shape of the decisions, not Adapto's literal schema syntax, so check the real docs before copying them into a request body. What is fact: Adapto ships three built-in types (Articles, Pages, and Categories), and when those don't cover what you need, custom collections let you define your own types on top of them, reachable via `/v1/custom-collections` and `/v1/custom-collections/{id}/items`. And however you shape any of it, the public API only reads that data back (GET); creating and updating entries happens through the `adapto` CLI, not a write endpoint you'd call from a frontend.

## Reuse and relationships: don't build ten of the same field

The payoff of references and components is that a change happens in one place. If your SEO fields are a component embedded in `article`, `page`, and `product`, updating the component's validation rules updates it everywhere those types use it. You're not hunting down three copies of a `meta_description` field with three slightly different max-lengths.

The same logic applies to references. If `Author` is its own type, updating an author's bio updates every article that references them, instantly, with no re-save required on the articles themselves. Duplicate the author's name and bio as plain text fields on every article instead, and you've created a data-consistency problem: nothing keeps those copies in sync, and a name change means finding every article that mentions that author.

The tradeoff to name: a reference is a runtime lookup. Resolving `article.author` means the API (or your frontend) has to fetch or join in the referenced entry, which costs a round trip or a resolver call depending on your API style. The exact cost differs between REST and GraphQL, which is the whole subject of [REST vs GraphQL for a headless CMS](/articles/rest-vs-graphql-headless-cms/). References buy you consistency; they cost you a lookup. Components cost nothing extra at fetch time because they're embedded in the parent entry, but they can't be queried or reused independently. Pick based on which property the data needs: independent lifecycle, or embedded convenience.

## Localization-aware modeling

The single most common content-modeling mistake in a multilingual project is building a separate content type per language: `ArticleEN`, `ArticleFR`, `ArticleDE`. It looks fine with two languages and becomes unmaintainable at five: every field change means editing N types, every reference has to be re-pointed per language, and your frontend routing logic has to know which type to query for which locale.

The fix is to make locale a property of the entry rather than a fork of the type: one `article` type, one entry per language sharing the same field structure, and a `locale` field (or a first-class locale dimension in your CMS) that the API filters or resolves on. Structure stays identical across languages; only the field *values* differ. That still leaves real questions that deserve their own answer: how do partial translations fall back to a default locale, and how do you avoid a network round trip per language switch on the frontend. We cover the full locale-aware model, fallback behavior, and delivery pattern in [modeling multilingual content without duplicating types](/articles/how-to-model-multilingual-content/).

## Five pitfalls that make a model expensive to change later

**1\. Over-nesting.** A component embedded inside a component embedded inside a component reads fine in a design doc and becomes a nightmare to query, validate, or migrate. If you're three levels deep (`article.hero.cta.button.icon`), you've built a tree instead of a content model. Flatten it. Most of the time the intermediate levels aren't earning their keep.

**2\. Per-locale duplication.** Covered above, worth repeating because it's the most common mistake in this category: `ArticleEN` / `ArticleFR` as separate types instead of one type with per-locale entries. It always starts reasonable ("we only have two languages") and always turns into a migration project.

**3\. God-types.** One `Page` type with forty optional fields to cover every layout the marketing team might someday want, instead of a handful of focused types (or a small set of composable "block" components). A god-type means every entry's edit screen shows thirty-five fields that don't apply to it, and every frontend render has to branch on which subset is populated. Split it: a lean `Page` type plus a `blocks` field that's an array of typed components (`hero`, `testimonial`, `cta`) is more work up front and far less work every time marketing asks for a new layout.

**4\. Required fields with no migration plan.** Marking a field `required` freezes every future schema change into a migration: you can't add a required field to a type with existing entries without backfilling every one of them first. Default new fields to optional, and only tighten to required once you've decided you're comfortable running that backfill.

**5\. References with no plan for deletion.** What happens to an `Article` when the `Author` it references gets deleted? If the answer is "the reference breaks," you've deferred a bug to whoever hits it in production. Decide up front (restrict deletion while references exist, cascade, or null out the reference) and treat it as part of the model from the start.

## FAQ

### What is content modeling?

Content modeling is designing the shape of your content before you author any of it: which content types exist, what fields each has, how types reference each other, and which field groups get reused as components. In a headless CMS it's the thing you build against the API: every frontend query, editor screen, and migration inherits whatever the model gets right or wrong.

### How do I structure content models in a headless CMS?

Start from what a page needs to render, rather than from a database-normalization instinct. Define a small set of content types (Article, Page, Author, Category), use typed fields for scalars, use reference fields to point at other types instead of duplicating their data, and pull repeated field groups (SEO metadata, a CTA block) into reusable components. Keep nesting shallow, two or three levels at most, and model locale as a property of an entry, not a separate type per language.

### What's the difference between a reference field and a component in a content model?

A reference field points at a separate entry with its own identity and lifecycle, like an Article referencing an Author, where the Author is edited independently and reused across many articles. A component is a reusable group of fields with no independent identity, like an SEO block embedded inside an Article. If the thing needs its own edit screen and gets reused across entries, reference it. If it's a repeated shape of fields, embed it as a component.

## Where to go from here

The fastest way to sanity-check a model is to write out the JSON payload for one real entry before you finalize the schema. If the payload feels awkward, the model is wrong, not the payload. From there, Adapto's data model docs cover the exact field types and validation options available for building this out. Out of the box, Adapto ships three built-in types (Articles, Pages, and Categories), and the custom collections API is where you define anything beyond those three: a `Product`, a `Testimonial`, an `Event`, whatever your model needs, using the same list-and-item API shape as the built-in types. One constraint to design around either way: the public API is read-only, so populating and editing entries (built-in or custom) happens through the `adapto` CLI, not a write endpoint your frontend calls directly.

Read next: [Adapto's data model reference](/docs/data-models/), [the custom collections API](/docs/custom-collections-api/), and, once you've got the basic types down, [modeling multilingual content without duplicating types](/articles/how-to-model-multilingual-content/).
