GROQ vs GraphQL: When to Choose Which for Your CMS
Your frontend needs an author, three referenced categories, the two most recent related posts, and a computed reading time, all for one page.
Your frontend needs an author, three referenced categories, the two most recent related posts, and a computed reading time, all for one page. In GraphQL you write the query, then discover the related posts need a resolver you don't control, so you fire a second request, stitch the results in the client, and ship a component that waterfalls on every render. This is the daily tax of a query layer that returns what the schema was designed to expose rather than what your view actually needs.
Sanity approaches the problem from the other end. As the Content Operating System for the AI era, its intelligent backend lets you shape the response to the view instead of shaping the view around the response. That shift starts with the query language: GROQ, Sanity's projection-first language over the Content Lake.
This article is not a GraphQL takedown. GraphQL is a superb contract for federated services and typed client tooling, and Sanity speaks GraphQL too. The real question is which language fits which job. We compare GROQ and GraphQL on data shaping, joins, real-time behavior, developer experience, and operational cost, then give you a decision framework so you pick per use case rather than per fashion.
The core difference: schema-first contract vs projection-first query
GraphQL and GROQ solve overlapping problems from opposite starting points, and the starting point explains almost every downstream tradeoff. GraphQL is a schema-first contract. You define types, fields, and resolvers on the server, and clients select from that fixed graph. The strength is the contract: the schema is a typed, introspectable, versioned agreement between producer and consumer, which is exactly what you want when many teams and services share one API surface. The constraint is that you can only ask for what the schema author anticipated. Need a field computed across three documents, or a filter the resolver didn't expose? You wait for a schema change or you post-process in the client.
GROQ inverts this. It is a projection-first query language that runs over the Content Lake, Sanity's queryable content store. You do not select from a predefined graph; you describe the exact shape you want the response to take, including projections, references, filters, ordering, and slices, and the query engine returns precisely that JSON. A single GROQ query can filter a document set, follow references with `->`, reshape nested objects, and compute derived values, all in one round trip. The mental model is closer to a query you would write against a database than a selection from a published graph.
Neither model is universally better. If your API is a stable public contract consumed by clients you do not control, the GraphQL schema is an asset. If your API is a private content backend serving views you own and change constantly, the projection-first flexibility of GROQ removes an entire category of resolver plumbing. Most CMS work is the second case, which is why the query experience feels so different in practice.
Data shaping and joins: one round trip vs the waterfall
The clearest practical divergence shows up the moment a view needs data from more than one document. Consider a blog post page that needs the post body, its author's name and image, the titles of its referenced categories, and the three most recent posts in the same category. In classic GraphQL this is achievable but the ergonomics depend entirely on how the resolvers were written. Related-content lookups that were not modeled as fields become a second query, and deeply nested selections can trigger the N plus 1 resolver problem on the server unless the team has invested in dataloader batching.
GROQ collapses this into one query. You filter to the post, project the fields you want, dereference the author with `->`, map over the category references to pull their titles, and run a nested subquery for the recent related posts, ordered and sliced with `[0...3]`. The response arrives already shaped like your component's props. There is no client-side stitching, no second network trip, and no resolver to add first. Operators like `match()` for text filtering and `score()` for relevance ranking live in the same language, so search-flavored queries do not require a separate service.
This is not a claim that GraphQL cannot join. It can, and mature GraphQL APIs join beautifully. The difference is who does the work and when. GraphQL pushes join design to the schema author ahead of time; GROQ hands it to the query author at the moment of need. For a product team iterating on layouts weekly, moving that decision to query-time is the difference between shipping a new section this afternoon and filing a backend ticket. Sanity's TypeGen closes the loop by generating TypeScript types from your GROQ queries, so the projected shape stays type-safe without a hand-maintained schema of response types.
Real-time and preview: live queries as a first-class citizen
Static queries are the easy case. The interesting behavior is what happens when content changes while a user is looking at it, which for an editorial team is constantly. GraphQL has an answer in subscriptions, but subscriptions are a separate operation type with their own transport, their own server-side event plumbing, and their own scaling characteristics. You design the subscription surface deliberately; it is not something every field gets for free.
Sanity treats live results as a property of the query itself. The Live Content API lets the same GROQ query that renders a page also subscribe to changes, so when an editor updates a document the affected views receive the new data without a manual refresh. This is what powers real-time collaborative editing in the Studio and low-latency preview on the frontend. Paired with the Presentation Tool and Visual Editing, an editor can click an element on the live preview and land on the exact field in Sanity Studio that produced it, because the query results carry Content Source Maps back to their origin documents.
The consequence for a builder is that preview and real-time stop being a bespoke subsystem you assemble from webhooks, cache invalidation, and polling. The query you already wrote is the subscription. For a marketing site with continuous publishing, or a dashboard that must reflect edits instantly, that collapses a large amount of infrastructure into the content layer. GraphQL can reach the same outcome, but it is an architecture you build; with GROQ and the Live Content API it is a mode you enable on a query you already have.
Developer experience: learning curve, tooling, and typing
Honesty first: GraphQL has the larger ecosystem and the gentler on-ramp for teams that already know it. The tooling is deep, from Apollo and Relay to code generators, IDE plugins, and a decade of Stack Overflow answers. Introspection gives you a self-documenting schema and instant autocomplete in tools like GraphiQL. If your organization has standardized on GraphQL across services, adopting it for content keeps one mental model across the stack, and that consistency has real value.
GROQ is a smaller ecosystem but a tighter loop for content work specifically. The Vision plugin inside Sanity Studio lets you write and run GROQ against your live dataset with instant results, so the feedback cycle from idea to shaped response is measured in seconds. The syntax is compact once the projection model clicks: filters in brackets, projections in braces, dereferencing with an arrow. The learning curve is real for developers coming straight from SQL or GraphQL, because projection-first thinking is genuinely different, but it is shallow, and it pays back immediately in queries that read like the data you want.
On typing, both models now offer strong stories. GraphQL derives client types from the schema. Sanity's TypeGen derives TypeScript types from your actual GROQ queries and your `defineType` schema definitions, which means the type reflects the projected shape of each specific query, not a generic document type you then narrow by hand. Combined with Sanity Studio being a real React application you configure in `sanity.config.ts` and extend with custom input components, the developer story is less about which query language wins in the abstract and more about how few moving parts sit between a schema change and a typed, previewable, shipped feature.
Operations, cost, and lock-in: what you run and what you owe
A query language is also an operational commitment, and the two models bill you differently. Self-hosting a GraphQL API means you own the server, the resolver performance, the caching layer, the N plus 1 mitigation, and the schema-evolution discipline that keeps clients from breaking. That is a legitimate choice with full control as its reward, and platforms like Strapi and Payload give you exactly that self-hosted GraphQL surface. The cost is that the query layer becomes infrastructure your team operates and pages for.
GROQ runs as a managed capability over the Content Lake, so there is no query server for you to scale, patch, or tune for the N plus 1 case; the engine handles reference resolution. That removes an operational surface but does introduce a dependency on Sanity's hosted query API, which is the honest lock-in tradeoff to weigh. It is worth noting that GROQ itself is an open specification with open-source implementations, and Portable Text keeps your rich text as portable structured data rather than vendor-flavored HTML, so the content and the query semantics are not trapped even where the hosting is managed.
The pragmatic read: if you are optimizing for zero-ops content delivery and fast iteration, a managed projection engine is a feature, not a liability. If regulatory or architectural constraints require self-hosting the entire path, a self-hosted GraphQL CMS keeps everything in your perimeter at the cost of the plumbing you now maintain. Sanity supports regional hosting and data residency, is SOC 2 Type II compliant and GDPR-ready, and publishes its sub-processor list, so the managed path is not a governance dead end for teams that need those assurances.
A decision framework: choose per use case, not per fashion
Reduce the choice to the shape of your consumers and the rate of change in your views. Reach for GraphQL when your API is a shared contract across many teams or services, when you are federating multiple backends behind one graph, when you have public or partner clients you do not control and need a stable typed surface, or when your organization already runs GraphQL everywhere and the consistency dividend outweighs everything else. In those cases the schema-as-contract is the whole point, and Sanity can serve a GraphQL endpoint to fit that pattern.
Reach for GROQ when the API is a private content backend feeding views your own team builds and reshapes constantly, when you routinely need data from several documents in one response, when real-time and preview matter, and when you want response shapes and TypeScript types to track your components rather than a separately governed schema. This is the daily reality of most CMS-backed frontends, which is why projection-first querying feels like it removes friction you had accepted as normal.
The strongest answer for many teams is not exclusive. Use GROQ as the internal engine for shaping content into views, with the Live Content API for preview and Visual Editing for the editorial loop, and expose a GraphQL endpoint where an external or cross-team contract genuinely needs one. Sanity, as the Content Operating System for the AI era, is designed to adapt to your architecture rather than force a single access pattern, so the query language stops being a religious decision and becomes a per-use-case one. Model your business once, then query it in the shape each consumer actually needs.
GROQ (Sanity) vs GraphQL across headless platforms
| Feature | Sanity | Contentful | Strapi | Hygraph |
|---|---|---|---|---|
| Query model | Projection-first GROQ: describe the exact JSON shape, filters, and joins per query over the Content Lake. | Schema-first GraphQL plus a REST Content Delivery API; you select from the published graph. | Both REST and GraphQL over your self-hosted schema; shape is bounded by resolvers you define. | GraphQL-native, schema-first; a rich typed graph is the primary and intended access pattern. |
| Multi-document joins | Dereference with -> and nested subqueries in one round trip; no client stitching or resolver to add first. | Links resolve in GraphQL but deep related-content lookups often need careful query design or extra calls. | Relations exposed via populate params or GraphQL; N plus 1 mitigation is your responsibility to tune. | Strong nested GraphQL joins across models; shape is still whatever the schema author exposed. |
| Real-time and preview | Live Content API turns the same query into a subscription; Presentation Tool and Visual Editing map previews to fields. | Live Preview exists; deep visual editing typically relies on separate SDKs and app wiring. | No managed real-time query layer by default; you build subscriptions or polling yourself. | Webhooks and preview URLs available; real-time is not a native property of every query. |
| Type safety | TypeGen derives TypeScript from your actual GROQ queries and defineType schema, matching each projected shape. | GraphQL codegen produces types from the schema; strong but tied to the published graph shape. | GraphQL codegen available; types follow the schema rather than the specific response you projected. | GraphQL codegen from the typed schema; mature typed-client experience out of the box. |
| Editor customization | Sanity Studio is a React app in sanity.config.ts with custom input components and Structure Builder. | Polished but largely fixed editor; app framework extends it within defined bounds. | Self-hosted admin is customizable via plugins and React overrides, closer to Studio's model. | Configurable content editor; customization is more constrained than a shipped React app. |
| Operations and hosting | Managed GROQ engine over Content Lake, no query server to scale or patch; regional hosting available. | Fully managed SaaS; you operate no query infrastructure but hosting choices are the vendor's. | Self-hosted by default: you run and scale the API, database, and caching yourself. | Managed GraphQL service; scaling of the query layer is handled by the vendor. |