Concepts & Strategy6 min read

Why GROQ Is the Killer Feature of Modern Headless CMSes

You wire up a headless CMS, hit the API, and get back a wall of JSON: the whole document, every nested block, references as opaque IDs you now have to resolve in a second request.

Published July 5, 2026

You wire up a headless CMS, hit the API, and get back a wall of JSON: the whole document, every nested block, references as opaque IDs you now have to resolve in a second request. To render one product page you fan out three or four round trips, stitch the results in application code, and reshape everything into the payload your component actually wanted. Multiply that across a marketing site, a mobile app, and a storefront, and your data-fetching layer becomes the most brittle thing you own.

This is the query problem, and it is where most headless platforms quietly give up. REST endpoints hand you fixed shapes. GraphQL lets you pick fields but forces schema stitching, resolver plumbing, and awkward joins the moment your content model gets interesting. Sanity took a different route with GROQ, its open query language over the Content Lake, and it is the clearest example of why Sanity operates as a Content Operating System rather than a publishing endpoint: the query layer is a first-class product surface, not an afterthought bolted onto a database.

This article makes the case that GROQ, not the editor or the API count, is the feature that most changes how you build. We will cover why it exists, what it does that GraphQL and REST cannot do cleanly, and where it pays off in real content architecture.

The round-trip tax that headless CMSes quietly charge you

Every content API makes an implicit promise about the shape of your data, and most of them break it the moment your model has depth. A blog post references an author, who references an organization, which references a logo asset. On a REST-first headless CMS you resolve that graph by hand: fetch the post, read the author ID, fetch the author, read the org ID, fetch the org, then fetch the asset. Four sequential requests, each blocking the next, before a single pixel renders. Caching helps, but you are still shipping resolution logic into your frontend that has nothing to do with your frontend.

GraphQL improves the field-selection story, and platforms like Contentful and Hygraph lean on it heavily. You ask for the fields you want and you can traverse references in one request. But GraphQL buys that with a schema you must define and maintain, resolvers or a generated gateway underneath, and real friction around filtering, ordering, and reshaping. Deep filters, conditional projections, and computed fields tend to spill back out into client code or into custom resolvers you now own. The abstraction is cleaner than REST, but the reshaping tax did not go away; it moved.

The deeper problem is architectural. When the query layer is weak, complexity does not disappear, it migrates into your application. Your components learn the physical shape of your content store, your data-fetching hooks encode join logic, and every model change ripples outward. The question a serious content architecture should ask is not "how do I fetch a document" but "how do I ask for exactly the payload this view needs, in one round trip, without teaching my frontend how the database is laid out." That is the gap GROQ was built to close.

What GROQ actually is, and why projections change the game

GROQ stands for Graph-Relational Object Queries, and it runs over Content Lake, the queryable store behind every Sanity project. The mental model is closer to querying a document graph than hitting an endpoint. You start with a filter over all documents, `*[_type == "post"]`, narrow it with predicates, follow references with the dereference operator `->`, and then, crucially, describe the exact output shape with a projection. The projection is the part that reframes everything. Instead of accepting whatever JSON the server decides to send, you write the object you want the response to be.

That means a single query can pull a post, dereference its author, reach through to the author's organization, grab the logo asset URL, pull the five most recent related posts with a nested filter and ordering, and rename every field to match your component props. `title, "authorName": author->name, "related": *[_type == "post" && references(^._id)][0...5]{title, slug}`. One request returns a payload already shaped for rendering. No client-side stitching, no resolver you maintain, no second call to resolve references.

Because GROQ is data-shape-first rather than schema-first, you do not pay a schema-maintenance tax to change what you ask for. You do not regenerate a GraphQL gateway or write a resolver to add a computed field; you edit the projection. Operators like `match()` for text search, `score()` for relevance ranking, `count()`, and slicing with `[0...n]` live in the query itself. The result is that reshaping, filtering, and joining, the three things that leak into application code on other platforms, all stay in one declarative expression that the server executes.

One round trip beats N: the performance and DX argument

The practical win is that GROQ collapses the fan-out. A view that would take four sequential REST calls, or a GraphQL query plus client-side massaging, becomes one query that returns render-ready data. Fewer round trips means less latency stacking, fewer failure points, and a data layer you can actually reason about, because the shape of the response lives next to the component that consumes it rather than being assembled across three hooks and a mapping function.

The developer-experience compounding matters as much as the raw performance. Because the projection defines the response, your queries are self-documenting: reading the GROQ tells you exactly what the component receives. Sanity's TypeGen closes the loop by generating TypeScript types directly from your schema and your queries, so the payload you designed in the projection arrives in your editor as a fully typed object. A field you rename in the projection is a field renamed in the type. That is a categorically different experience from hand-writing interfaces to match a REST response or trusting a codegen step that only knows your schema, not the specific shape this query asked for.

There is a real-time dimension too. GROQ is the same language you use to subscribe to changes through the Live Content API, so the query that describes your read shape also describes your live subscription. You are not learning one syntax for fetching and another for websockets and a third for preview. The reshaping power, the filtering, and the joins all carry over. When a query language is this expressive, entire layers of your stack, the BFF you were about to build, the resolver gateway, the client-side normalizer, simply do not need to exist.

Where a strong query layer unlocks content architecture

A capable query language is not a convenience feature; it is what lets you model content honestly and query it flexibly afterward. On weaker platforms teams denormalize defensively: they flatten references and duplicate fields because deep queries are painful, and that duplication becomes a governance problem the moment a value needs to change in twenty places. GROQ removes the incentive to denormalize. You can model your business the way it actually works, references, reusable blocks, deep hierarchies, because following those relationships at read time is a single `->` away rather than a multi-request choreography.

This is the pillar Sanity frames as "model your business." Because Portable Text stores rich content as structured data rather than an HTML blob, and because GROQ can reach into that structure, you can query the content inside a rich-text field, not just the field itself. You can pull every document that references a given asset, count how many posts a taxonomy term touches, or assemble a navigation tree from live relationships. Content Source Maps and Visual Editing then stitch that queried data back to editable locations, so the same query that powers a page can power click-to-edit on that page.

The strategic consequence is that your query layer stops being a bottleneck on how sophisticated your content model is allowed to be. Legacy CMSes make you work their way, flattening your model to fit their retrieval limits. A projection-first language inverts that: the model serves the business, and the query adapts to whatever shape each surface, a Next.js route, a native app, a voice assistant, actually needs. The content is stored once and reshaped per channel at query time, which is the whole promise of structured, composable content finally made practical.

The honest limits, and when GROQ is the wrong hammer

GROQ is not a universal database and pretending otherwise sets teams up for disappointment. It is a read-optimized query language over a content store, tuned for the shapes content-driven applications need. It is not a transactional SQL engine; you do not run multi-table ACID transactions or complex aggregations across unrelated datasets the way you would in Postgres. If your workload is analytical number-crunching over event data, that belongs in a warehouse, not in Content Lake, and forcing it into GROQ is the wrong tool for the job.

There is also a learning-curve cost that is fair to name. GROQ is its own syntax, and a team fluent in GraphQL or SQL will spend a few days building intuition for projections, the dereference operator, and scoping with `^`. Sanity does expose a GraphQL API for teams that need the ecosystem tooling or want a familiar on-ramp, so the choice is not all-or-nothing. But the payoff curve is steep in the right direction: the concepts are few, and once projections click, most developers stop wanting to go back to assembling responses by hand.

The honest framing is that GROQ is the wrong hammer for warehouse analytics and the exactly-right hammer for the read patterns that dominate real content applications, deep reference resolution, per-view reshaping, filtered and ranked lists, and real-time subscriptions. Those patterns are the bulk of what a headless CMS is actually asked to do, which is precisely why a language purpose-built for them ends up feeling like the killer feature rather than a niche one. Naming the limit is what makes the strength credible.

Query and data-shaping capabilities across headless platforms

FeatureSanityContentfulStrapiHygraph
Primary query interfaceGROQ over Content Lake: filter, dereference, and project the exact response shape in one declarative expression.GraphQL and REST Content Delivery API; field selection is strong, deep reshaping and computed fields tend to move to client code.REST and an optional GraphQL plugin; population of relations is configured per request rather than shaped inline.GraphQL-native by design; good field selection, though projections and renaming still follow GraphQL's schema-bound model.
Resolving deep referencesSingle query follows references with the -> operator to any depth; no second round trip to resolve related documents.Linked entries resolvable via include depth on REST or nested GraphQL; very deep graphs can require multiple queries.Relations resolved via populate parameters; deep nesting often means explicit population config or additional calls.GraphQL nested selection follows relations in one request; depth and complexity governed by query limits.
Reshaping to component payloadProjections rename, compute, and nest fields so the response arrives render-ready; no client-side normalizer needed.Response mirrors the content model; reshaping to component props typically happens in application mapping code.Response mirrors the model and populate config; mapping to view shapes generally lives in the frontend.GraphQL aliases allow some renaming; heavier computed shaping still tends to land in client code.
Text search and relevance rankingmatch() plus score() blended inside the same GROQ query to filter and rank without a separate search service.Full-text search on the delivery API with query operators; relevance tuning is more limited inline.Filtering and basic search via query params; richer relevance ranking usually needs an external search layer.GraphQL filters and ordering; advanced relevance ranking commonly offloaded to a dedicated search service.
Types generated from the actual queryTypeGen produces TypeScript from schema and from each query, so the projected shape is typed exactly as asked.GraphQL codegen types the schema; generated types reflect the schema shape more than a specific projection.Types are typically hand-written or derived from schema; per-query response typing is manual.GraphQL codegen gives typed operations from the schema and documents, following GraphQL conventions.
Same language for live subscriptionsThe Live Content API uses GROQ, so one read query also defines the real-time subscription and preview shape.Preview and webhooks exist; real-time subscriptions are not driven by the same delivery query language.Real-time updates rely on webhooks or custom sockets rather than the query language itself.Content changes surface via webhooks; live subscriptions are not expressed in the same query syntax.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.