GROQ for GraphQL developers
Why don't we use GraphQL as our primary query language, and what to do if you want a GraphQL API on your data
If you're familiar with GraphQL, GROQ will feel familiar in some places and different in others. This page maps common GraphQL patterns to their GROQ equivalents and highlights what GROQ adds beyond what the GraphQL spec describes. If you want to use Sanity's GraphQL API instead, see the GraphQL guide.
GROQ is the native query language for the Sanity Content Lake. GraphQL is a generated, typed layer based on your schema. This guide is here to help GraphQL users decide whether to switch and to translate what they already know.
GraphQL concepts in GROQ
Fetch by ID
In GraphQL, you call a schema-declared root field. In GROQ, you filter the document space and project the fields you want. Any field can be the lookup key, not only _id.
{
Post(id: "abc") {
title
}
}*[_id == "abc"][0]{title}Field selection
GraphQL selection sets become GROQ projections. GROQ lets you rename fields inline ("heading": title) and spread the rest with ....
{
allPost {
title
body
}
}*[_type == "post"]{title, body}Filtering
GraphQL where arguments are schema-declared. GROQ filters are arbitrary boolean expressions inside [ ]. You can combine equality, comparison, match, in, defined(), references(), and date math without changing the schema.
{
allPost(where: {title: {matches: "hello"}}) {
title
}
}*[_type == "post" && title match "hello*"]{title}Reference traversal
GraphQL traverses references through nested selection on schema-declared relation fields. GROQ uses the -> operator on any field that contains a _ref value, with no schema declaration required. Chains compose: author->company->address.
{
allPost {
author {
name
bio
}
}
}*[_type == "post"]{author->{name, bio}}Pagination
Sanity's GraphQL API exposes limit and offset arguments on list fields. GROQ uses slice syntax, which works on any array, including sub-arrays inside projections.
{
allPost(limit: 10, offset: 0) {
title
}
}*[_type == "post"][0...10]{title}Ordering
GraphQL passes sort arguments to a list field. GROQ uses the | order() pipe function, which supports multi-key ordering and inline transforms such as lower(title) for case-insensitive sorting.
{
allPost(sort: [{title: ASC}]) {
title
}
}*[_type == "post"] | order(title asc){title}Aggregation
GraphQL has no aggregation in the query language. GROQ has count(), math::sum(), math::avg(), math::min(), and math::max() as first-class functions you can inline in projections. Neither language has a native group-by.
{
"orderCount": count(*[_type == "order"]),
"totalRevenue": math::sum(*[_type == "order"].amount)
}Real-time updates
GraphQL subscriptions are schema-declared and limited to a single root field per operation. Sanity does not support GraphQL subscriptions, but offers two real-time options driven by GROQ filters: the Listen API for server-sent change events on any GROQ filter, and the Live Content API, which handles caching and reconnection for production workloads.
Typed queries
GraphQL fragments plus a codegen pipeline give you typed clients. The equivalent in Sanity is defineQuery() from the groq package combined with Sanity TypeGen, which generates TypeScript result types from your queries automatically. Query reuse in GROQ is achieved with string composition or GROQ custom functions.
import {defineQuery} from 'groq'
const postsQuery = defineQuery(`*[_type == "post"]{title, body}`)
const posts = await client.fetch(postsQuery)
// posts is fully typed when TypeGen runsWhat GROQ adds beyond GraphQL
Some capabilities are inline in GROQ that would require server-side resolver code in a GraphQL implementation, and one is specific to Sanity's platform regardless of the query language.
Inline computed fields
GraphQL is not a computation language; computed values must be defined as schema fields with server-side resolvers. GROQ lets you compute values directly in the projection with coalesce(), select(), string functions, math functions, and arbitrary expressions.
*[_type == "product"]{
title,
"displayPrice": coalesce(salePrice, regularPrice),
"label": select(stock > 0 => "In stock", "Out of stock"),
"reviewCount": count(reviews)
}Dynamic reference traversal
GraphQL requires every join point to be a schema-declared field with a resolver. GROQ traverses any _ref value at query time, with no schema changes or server redeploy. Adding a new traversal is a query edit, not a deployment.
Reverse lookups in projections
GROQ's ^ parent reference and references() function let you express a reverse join inside a projection, without a schema-declared reverse relation. In GraphQL, the same query requires a schema field with a resolver that searches for documents referring to the current one.
*[_type == "author"]{
name,
"posts": *[_type == "post" && references(^._id)]{title, publishedAt}
}Full-text and semantic search
GROQ provides text::query() for structured text search with phrase, prefix, and negation syntax, and text::semanticSimilarity() for vector-based ranking. GraphQL has no text or semantic search in the spec; implementations need custom resolvers or external integrations. Semantic similarity requires dataset embeddings to be enabled.
*[_type == "article" && body match text::query("machine learning -python")]
*[_type == "article"] | score(text::semanticSimilarity("how to handle authentication"))Cross-dataset references
Sanity's cross-dataset references let a document in one dataset link to a document in another. GROQ traverses them with the same -> operator. Sanity's GraphQL API does not support cross-dataset references; only GROQ can dereference them. Cross-dataset references are available on the Enterprise plan.
Mutations
Sanity's GraphQL API does not support mutations. Writes go through the Mutation API (or the equivalent client methods such as client.create(), client.patch(), and client.delete()). This is the same for GROQ users and GraphQL users; mutations are a separate concern from queries.
When to keep GraphQL
Some projects are better served by Sanity's GraphQL API:
- You have an existing investment in GraphQL tooling such as Apollo Client, Relay, or established codegen pipelines that you want to keep using.
- Your team has deep GraphQL expertise and the cost of switching mental models would outweigh the gain.
See the GraphQL guide for setup, deployment, perspectives, security, and the full filter and sort reference.
Next steps
- GROQ introduction: the tutorial that walks through the language one concept at a time.
- GROQ query cheat sheet: one-page reference for the most common query shapes.
- GROQ functions reference: every built-in function, including the math, text, string, and array namespaces.
- Sanity TypeGen: typed clients for GROQ queries, the equivalent of GraphQL codegen.