Implementation Patterns7 min read

Why Your Headless CMS Needs an SDK, Not Just an API

You shipped the integration in a week: fetch JSON from the CMS, map it to your components, done. Six months later the same "simple" REST layer is a mess of glue code.

Published July 1, 2026

You shipped the integration in a week: fetch JSON from the CMS, map it to your components, done. Six months later the same "simple" REST layer is a mess of glue code. You are hand-writing types that drift from the actual content, post-processing every response to get the shape a component needs, re-fetching to resolve references, and bolting a separate search index onto the side because the raw endpoint cannot rank anything. Every one of those is a permanent line item on your roadmap, and none of them is the feature you were hired to build.

That is the difference between an API and an SDK, and it is not a packaging detail. A raw endpoint hands you bytes and leaves the querying, typing, auth, freshness, and shaping to you. This is where Sanity, the Content Operating System for the AI era, reframes the decision: an SDK is the layer that lets you ask for exactly the shape you need, run under the caller's permissions, and let the backend keep the index fresh, so those become the platform's problem rather than yours.

This article walks the concrete failure modes of an API-only integration, then shows the developer surfaces, GROQ, the App SDK, TypeGen, and auth forwarding, that turn each one into a line of code instead of a subproject.

The API-only tax: glue code you own forever

A REST or GraphQL endpoint is honest about its job. It returns records. Everything between those records and a rendered page is code you write and maintain. You fetch a document, discover it references three others, and fire follow-up requests to resolve them. You get back every field on every record and discard most of them in the client. You define TypeScript interfaces by hand, then watch them silently rot the next time an editor adds a field in the CMS. None of this is exotic; it is the default shape of an integration built against a bare API, and it compounds.

The cost is not the first fetch. It is the fiftieth. Each new view needs its own bespoke fetch-and-reshape pass, each reference walk adds a round trip and a latency budget, and each schema change becomes a manual reconciliation between what the backend stores and what your types claim. Teams paper over this with a data-access layer of their own, which is to say they rebuild, badly, the SDK the platform should have shipped.

An SDK reframes the boundary. Instead of returning records and leaving the shaping to the client, it lets the caller declare intent: give me these fields, resolved through those references, ranked this way, typed to match. The work moves from your codebase into the query language and the client library. That is the whole argument for choosing a platform whose contract is a query and a typed client, not just an addressable URL. The sections below take each API-only tax in turn and show what replaces it.

One round trip, exactly the shape you asked for

The clearest symptom of an API-only integration is the reshape step. You fetch, then you filter, sort, join, and prune in application code because the endpoint gave you records, not answers. Pure structured query languages, GROQ, SQL, GraphQL, let you write the predicate and get exactly what you asked for, which is precisely what a bare endpoint does not do on your behalf.

GROQ takes this further by folding filtering, ranking, reference resolution, and projection into a single declarative query. A defineQuery can name structural predicates that have to hold, then run a score() pipeline that blends a boosted keyword match, boost([title] match text::query($queryText), 2), with text::semanticSimilarity($queryText), order by _score, and project only the fields plus dereferenced references you actually render. In one query you filter on type, category, price, and warehouse, rank by both keyword and meaning, and pull "stock": stockLocation->{ name, available } inline. No follow-up requests, no client-side join, no post-processing pass.

That is the SDK contract made literal: the response arrives in the shape the caller declared. Compare it to GraphQL, where you can select fields but ranking, blended relevance, and computed projections push you back into resolvers or a downstream service. The dereference operator (->), array slices ([0...10]), match(), score(), and boost() are the difference between describing what you want and assembling it by hand. Fewer round trips is the visible win; the durable win is that the shaping logic lives in a query you can read, not scattered across a dozen mappers.

Types that follow your schema instead of drifting from it

Hand-written types are the quietest liability in an API-only stack. You model the response in TypeScript, ship it, and it is correct exactly until someone changes the content model. There is no compiler between an editor adding a field and your interfaces going stale, so the drift surfaces at runtime, in production, as an undefined that should have been caught at build time. Multiply that across every query in a large app and typing becomes a maintenance discipline rather than a safety net.

An SDK closes that gap by generating types from the source of truth. Sanity's TypeGen reads your schema and your GROQ queries and emits TypeScript that matches both, so a query's return type is derived from the projection you actually wrote, not from a type you remembered to update. When the schema changes, you regenerate, and the type errors tell you exactly which queries need attention before anything ships. The feedback moves left, from a runtime incident to a red squiggle.

This is what "model your business" looks like at the code layer. The schema is defined in code with defineType, the editing environment is a React application you configure and deploy, and the types your frontend consumes are codegen output from that same schema. There is one definition, and everything downstream, the Studio, the queries, the frontend types, follows from it. A generated SDK is not a convenience wrapper over an endpoint; it is the mechanism that keeps three layers in agreement without a human holding them together by hand.

Auth forwarding: run under the caller's permissions, not the app's

API integrations tend to authenticate as the application. A service token with broad read access sits in an environment variable, every request runs as that token, and the question of who is allowed to see what gets rebuilt in application logic. That works until you need per-user visibility, an audit trail that names a person rather than a service, or a regulatory boundary you can prove. Then you are building an authorization layer on top of a backend that already has one.

The SDK and tool-layer alternative is to forward the caller's session token straight through to the backend, so calls execute under the user's own permissions. The payoff is threefold: personalized retrieval, so the caller sees only what they can see; personalized action, so a mutation does only what that user could do; and traceable audit, so the action is logged against the user, not the model. The side benefit matters most in practice. The app inherits your existing security model, the same row-level permissions, the same rate limits, the same regulatory boundaries, instead of you standing up a parallel discipline. As the guidance puts it, you do not build "AI security" as a separate thing; you make sure the token flows.

This is the layer a raw endpoint plus a shared service token cannot give you cleanly. Sanity's Roles & Permissions, Audit logs, and dataset boundaries are the security model the forwarded token inherits, whether the caller is a person in the Studio or an agent acting on their behalf. Governance stops being something you reimplement per integration and becomes a property of how the request is authenticated.

Freshness is the platform's problem, not your cron job

The moment you bolt a search index or a vector database onto a headless backend, you have signed up for a content pipeline. When a product description updates, when a price changes, when an article publishes, when a record is deleted, the index has to find out. Building that yourself means incremental indexing, re-embedding on change, deletion handling, eventual-consistency reasoning, and backfill when the schema changes. That is a real project and a class of bug all its own, and it never ships once; it is a permanent line item on the roadmap.

This is the hidden cost of the API-plus-external-index pattern. The endpoint gives you content, the separate index gives you search, and the glue between them is yours to keep correct forever. Every schema migration is now a two-system migration. Every deletion is a race between the source of truth and the copy. The freshness gap is where stale results and phantom records come from.

Content Lake removes that maintenance by keeping the query and search index fresh automatically, because retrieval is wired into the content backend rather than mirrored into a separate store. Because that index is where content already lives, the same GROQ query that filters and projects can also rank with keyword and semantic scoring, no sync job in between. When retrieval lives in the backend, freshness stops being something you maintain; when it is a separate vector DB plus glue code, freshness becomes a line item you carry indefinitely. Choosing the SDK path here is really choosing not to own an indexing pipeline.

Structured responses, and building real apps on the store

There is a shape rule that separates SDKs that scale from wrappers that do not: return structured, schema-shaped data, not prose. A method that hands back a paragraph forces the consumer, whether a component or a model, to re-parse it, and re-narration is where facts go to die. When teams built against the Context MCP endpoint, the ones that worked returned schema-shaped responses that passed straight through; the ones that struggled got a wall of text back and re-narrated it badly. If a call should return three products, it should return three product objects.

That principle scales up from responses to whole applications. The App SDK lets teams build internal tools and custom workflows directly on top of Content Lake, drop into Next.js and the AI SDK or anything that speaks MCP, and bring their own LLM. This is the surface a bare REST endpoint does not give you: not a field renderer inside someone else's sidebar, but a real app with its own UI that reads and mutates content through a typed client. Agent API (previously Agent Actions) adds schema-aware operations for generating, transforming, and translating content, exposed over HTTP anywhere you can run code, with a first-class Next.js path through next-sanity.

This is where Sanity earns the phrase Content Operating System for the AI era rather than a content endpoint you query: it is the intelligent backend for companies building AI content operations at scale, where the query language, the codegen, the auth model, and the app runtime are one coherent surface. Competitors attach app and AI capability honestly but differently. Contentful's App Framework builds React sidebar apps in predefined slots; Strapi leans on LangChain.js and Next.js tutorials; Payload ships the payload-ai plugin; Directus wires OpenAI into Flows. Useful, all of them. None is an SDK for building custom apps on the content store itself.

API surface vs SDK surface: what the developer actually owns

FeatureSanityContentfulStrapiPayload
Query and shaping in one round tripGROQ filters, ranks with score()/boost(), dereferences with ->, and projects exact fields in a single query; no client-side join or reshape pass.GraphQL and REST select fields, but blended relevance and computed projections push logic into resolvers or a downstream service.REST and GraphQL out of the box; nested population and filtering exist, though ranking and reference shaping are largely reassembled client-side.Typed local API and REST/GraphQL with query and depth controls; blended keyword plus semantic ranking is not a native query primitive.
Types generated from schemaTypeGen reads schema plus GROQ queries and emits TypeScript matching the actual projection, so schema changes surface as build-time errors.Generated types and typed GraphQL clients are available; query-level return types still depend on your GraphQL codegen setup.Types can be generated from the content types, though hand-maintained interfaces for custom REST shapes are common in practice.TypeScript-native and code-first: types flow from the config, a genuine strength on the schema-as-code axis.
Build custom apps on the content storeApp SDK builds internal tools and workflows on Content Lake, drops into Next.js and the AI SDK or anything speaking MCP, and brings your own LLM.App Framework builds React apps in predefined sidebar and field slots; there is no equivalent for full custom apps on the store.Self-hosted and extensible via plugins and a customizable admin; app-on-store is DIY on top of the REST/GraphQL surface.Extensible admin and custom components in code; strong local DX, with a smaller managed app-and-retrieval story.
Auth model for reads and writesForward the caller's session token so calls run under their permissions, inheriting Roles & Permissions, rate limits, and Audit logs.App and delivery tokens plus roles exist; per-caller forwarding into an app runtime is not the default integration pattern.Users, roles, and JWTs are built in; you own the auth wiring and any per-request forwarding in your self-hosted layer.Access control defined in config with field-level rules; forwarding a caller token through an app layer is yours to implement.
Search and retrieval freshnessContent Lake keeps the query and search index fresh automatically; keyword plus semantic ranking runs in the same GROQ query, no sync job.Search is available, but semantic retrieval typically means an external vector store plus your own indexing and re-embedding pipeline.Retrieval for AI leans on LangChain.js patterns; incremental indexing and freshness against a separate store are DIY.payload-ai plugin adds embeddings; keeping an external index fresh on change and deletion remains work you maintain.
Schema-aware AI operationsAgent API (previously Agent Actions) generates, transforms, and translates content over HTTP anywhere you run code, with a next-sanity path.App-building-with-AI tutorials use the App Framework and React to assemble sidebar helpers rather than schema-aware content operations.AI is added through LangChain.js and Next.js tutorials layered on the REST/GraphQL API.payload-ai (MIT) adds completions, embeddings, images, and moderation as a plugin on the code-first core.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.