Content types as API contracts
Content types as API contracts
TL;DR
- Every field UID you define becomes an exact key in the API response -- treat UIDs as public API surface, not internal details.
- Changing a field UID or field type on a content type with published entries is a breaking change for every frontend consumer.
- Adding fields is safe (existing entries return null); removing or renaming fields requires coordinated frontend deployment.
- Use the CMA to audit content type schemas and generate TypeScript types for compile-time safety.
Every content type you create in Contentstack simultaneously defines two things: a form that editors fill out and a JSON schema that frontend developers code against. The moment you save a content type with a field UID of short_description, that string becomes a key in every API response for that content type - and every frontend component that reads entry.short_description depends on it. Content types are not just editorial structures; they are API contracts, and treating them as such prevents the kind of breaking changes that send frontend developers scrambling after a "simple" content model update.
[Image of Content type dual nature showing how a single schema maps to an editorial entry form and an API JSON response payload]The content type defines the API shape
In Contentstack, a content type is a JSON schema stored in your stack. You build it through the UI at Content Models > + New Content Type, or you define it programmatically via the Content Management API. Either way, the result is the same: a schema that dictates what fields exist, what types they have, and what UIDs identify them in API responses.
Consider the Veda jewelry catalog. You create a Product content type with these fields:
| Field label | Field UID | Field type | Notes |
|---|---|---|---|
| Title | title | Single Line | Required, unique |
| URL | url | Single Line | URL path for routing |
| Short Description | short_description | Multi Line | Summary for cards |
| Description | description | Multi Line | Longer product copy |
| Price | price | Number | Price in currency units |
| Media | media | File, multiple | Product images |
| Product Line | product_line | Reference | Multiple, references Product Line type |
| Category | category | Reference | Multiple, references Category type |
The Product Line reference points to entries with:
| Field label | Field UID | Field type |
|---|---|---|
| Title | title | Single Line |
| URL | url | Single Line |
| Description | description | Multi Line |
| Image | image | File |
Each field UID becomes an exact key in the JSON that the Content Delivery API returns. The field label ("Short Description") is what editors see in the UI; the field UID (short_description) is what developers see in code. This distinction matters: labels can change freely to improve the editing experience, but UIDs are part of the contract.
flowchart LR
A[Content Type Schema
Field UIDs + Types] --> B[Editor Form
Display Names + Help Text]
A --> C[API Response
JSON with field UIDs as keys]
A --> D[Frontend Code
TypeScript interfaces]The JSON schema behind a content type
When you export a content type or fetch it via the Content Management API at GET /v3/content_types/product, you get back the full schema definition. Here is a simplified version of the Product content type schema:
{
"content_type": {
"title": "Product",
"uid": "product",
"schema": [
{
"display_name": "Title",
"uid": "title",
"data_type": "text",
"mandatory": true,
"unique": true,
"field_metadata": { "_default": true }
},
{
"display_name": "URL",
"uid": "url",
"data_type": "text"
},
{
"display_name": "Short Description",
"uid": "short_description",
"data_type": "text",
"field_metadata": { "multiline": true }
},
{
"display_name": "Description",
"uid": "description",
"data_type": "text",
"field_metadata": { "multiline": true }
},
{
"display_name": "Price",
"uid": "price",
"data_type": "number"
},
{
"display_name": "Media",
"uid": "media",
"data_type": "file",
"multiple": true
},
{
"display_name": "Product Line",
"uid": "product_line",
"data_type": "reference",
"reference_to": ["product_line"],
"multiple": true
},
{
"display_name": "Category",
"uid": "category",
"data_type": "reference",
"reference_to": ["category"],
"multiple": true
}
]
}
}Notice how the schema is a flat array of field definitions at the top level, with group fields nesting their own schema array. The data_type values (text, isodate, number, boolean, file, reference, json, group) map directly to how the data appears in API responses. This schema is the single source of truth for both the editorial form and the API output.
The actual API response
When a frontend developer fetches a Product entry from the Content Delivery API using GET /v3/content_types/product/entries/{entry_uid}, the response mirrors the schema exactly:
{
"entry": {
"uid": "blt_matrix_link_001",
"title": "Matrix Link Bracelet",
"url": "/products/digital-dawn/matrix-link-bracelet",
"short_description": "A sleek link bracelet composed of interlocking square links...",
"price": 295,
"product_line": [
{
"uid": "blt_digital_dawn_001",
"_content_type_uid": "product_line"
}
],
"category": [
{
"uid": "blt_bracelets_001",
"_content_type_uid": "category"
}
],
"description": "Crafted in sterling silver and gold tones with geometric detailing.",
"media": [
{
"uid": "bltasset001",
"url": "https://images.contentstack.io/v3/assets/.../matrix-link-bracelet.jpg",
"filename": "matrix-link-bracelet.jpg",
"content_type": "image/jpeg"
}
],
"locale": "en-us",
"created_at": "2025-01-10T14:30:00.000Z",
"updated_at": "2025-02-01T09:15:00.000Z"
}
}
Several things to note about this response:
- Field UIDs are the JSON keys. short_description in the schema produces "short_description" in the response. There is no transformation or renaming.
- Reference fields appear as stubs by default. The product_line and category arrays contain only UIDs and content type identifiers. To get the full data, add include[]=product_line&include[]=category to the API request. See lesson 2.2.1 for details on reference resolution.
- File fields include metadata. Each item in the media array returns the asset's URL, filename, and MIME type.
- Field types drive output shape. In the current Veda implementation, description is modeled as multi-line text, so the API returns a string. If you instead choose JSON RTE for a field, the API returns a structured document tree. Lesson 2.1.4 covers that format.
- System fields are included automatically. Fields like uid, locale, created_at, and updated_at appear without being defined in the schema.
Field types and their API representations
Understanding how each field type maps to its API output is essential for frontend development. Here is the complete mapping:
| Field type | data_type value | API output type | Example value |
|---|---|---|---|
| Single Line Text | text | String | "Matrix Link Bracelet" |
| Multi Line Text | text | String | "Line 1\nLine 2" |
| Rich Text (HTML) | text | HTML string | "<p>Hello <strong>world</strong></p>" |
| JSON RTE | json | Document object | { "type": "doc", "children": [...] } |
| Markdown | text | Markdown string | "## Heading\nParagraph text" |
| Number | number | Number | 295 |
| Boolean | boolean | Boolean | true |
| Date | isodate | ISO 8601 string | "2025-09-15T09:00:00.000Z" |
| File | file | Object (url, metadata) | { "url": "...", "filename": "..." } |
| Reference | reference | Array of stubs/objects | [{ "uid": "...", "_content_type_uid": "..." }] |
| Group | group | Nested object | { "name": "...", "address": "..." } |
| Modular Blocks | blocks | Array of block objects | [{ "hero": { ... } }, { "cta": { ... } }] |
| Select | text | String or array | "featured" or ["a", "b"] |
| Link | link | Object | { "title": "...", "href": "..." } |
Frontend developers should keep this table as a reference. When a designer asks for a new field, the developer can immediately predict the API output format and start building the component before any content is entered.
Field UIDs: the keys to the contract
Common Pitfall
Renaming a field UID on a content type with published entries breaks every frontend component that references the old key -- the old key vanishes from API responses immediately.
Field UIDs deserve special attention because they are the most visible part of the API contract. When you create a field in the Contentstack UI, the system auto-generates a UID from the display name ("Event Date" becomes event_date). You can customize the UID at creation time, but once the content type is saved and entries exist, changing a field UID is a breaking change.
Best practices for field UIDs:
- Use snake_case consistently. Contentstack defaults to snake_case (event_date, banner_image). Stick with this convention across all content types.
- Be descriptive but concise. short_description is better than desc (too generic) or product_short_description_for_cards (too verbose).
- Avoid abbreviations that only your team understands. desc might mean "description" to you but confuses new developers.
- Namespace shared concepts. If multiple content types have an image field, use banner_image, thumbnail_image, or hero_image rather than just image everywhere. For Veda, media on Product and image on Product Line serve different purposes.
The contract in practice: frontend dependencies
Consider a React component that renders a Veda Product card:
interface Product {
uid: string;
title: string;
price: number;
short_description: string;
media: Array<{ url: string }>;
product_line: Array<{ title: string }>;
}
function ProductCard({ product }: { product: Product }) {
const image = product.media?.[0];
return (
{image && }
);
}This component depends on the exact field UIDs title, price, short_description, media, and product_line. If someone renames short_description to summary in the content type, this component breaks. If someone changes price from a Number to a Single Line Text field, the $ formatting may behave unexpectedly because the value changes from 295 to "295".
This is why content types are contracts. Both parties - the content modeling team and the frontend development team - must agree on the field UIDs and types before either side builds against them.
Versioning: what happens when the contract changes
Content models evolve. New features require new fields, and old fields sometimes become obsolete. Contentstack handles this with additive flexibility and a few constraints:
Adding a new field is generally safe. Existing entries return null or are absent for the new field until editors populate them. Frontend code should handle missing fields gracefully:
// Safe: handles missing field
{product.media?.[0] ? <img src="{product.media[0].url}" alt="{product.title}"> : "Image coming soon"}Removing a field is a breaking change. If a frontend component reads product.media[0].url and you delete the media field from the content type, the component throws a runtime error. Before removing a field:
- Confirm no frontend code references the field UID.
- Deploy a frontend update that removes the dependency.
- Only then delete the field from the content type.
Changing a field's type is risky. Converting a Single Line Text field to a Number field changes the API output from a string to a number. Code that calls .toLowerCase() on the value will crash. Treat type changes as a remove-and-add operation: deprecate the old field, add a new one with the correct type, migrate content, update frontends, then remove the old field.
Renaming a field UID breaks the contract immediately. The old key disappears from API responses and the new key appears. Every line of frontend code that referenced the old key fails. Avoid UID renames on content types that have published entries. If you need to rename, coordinate the change with a simultaneous frontend deployment.
Reordering fields has no API impact. Field order in the schema affects the editorial UI (the order editors see fields in the form) but does not change the JSON response structure. Reorder freely to improve the editing experience.
Using the Content Management API to inspect contracts
You can programmatically audit your content type contracts using the Content Management API. This is useful for building CI checks or documentation generators:
# Fetch a content type schema
curl -s -X GET "https://api.contentstack.io/v3/content_types/product" \
-H "api_key: YOUR_STACK_API_KEY" \
-H "authorization: YOUR_MANAGEMENT_TOKEN" \
-H "Content-Type: application/json" | jq '.content_type.schema[] | {uid, data_type, mandatory}'This outputs a list of field UIDs, types, and whether they are required - exactly the information a frontend developer needs to build TypeScript interfaces or validate data at the integration boundary.
Some teams go further and generate TypeScript types directly from the content type schema, ensuring compile-time safety. The Contentstack CLI's cs:content-type:get command can export schemas in a format suitable for code generation. See lesson 3.3.3 on Contentstack CLI for details.
Common mistakes
- Treating field UIDs as internal details. Field UIDs are not just database column names; they are public API field names. Choosing a UID like f1 or temp_field makes the API response unreadable and forces frontend developers to guess what the field contains. Use meaningful, stable UIDs from the start.
- Changing field types without coordinating with frontend teams. Converting a Date field to a Single Line Text field because "editors want more flexibility" changes the API output from an ISO 8601 string to freeform text. The frontend date formatter breaks, and invalid dates go undetected. If the field type must change, treat it as a contract renegotiation.
- Ignoring optional fields in frontend code. When a new field is added to a content type, existing entries do not have a value for it. Frontend components that assume every field has a value will crash with "Cannot read property of undefined" errors. Always write defensive code that handles null or missing fields.