Type something to search...
GROQ vs GraphQL: Which Should You Use With Sanity?

GROQ vs GraphQL: Which Should You Use With Sanity?

For most Sanity projects, GROQ vs GraphQL has a clear answer: use GROQ. It works out of the box with no deployment step, can filter, join, reshape and compute data in a single query, and is what Sanity's own tooling, from next-sanity to the Live Content API and TypeGen, is built around. Choose Sanity's GraphQL API when your team or stack is already committed to GraphQL, for example a front end built on Apollo or a gateway that stitches several GraphQL services together, and you're happy to redeploy the API whenever the schema changes.

Both languages read from the same Content Lake, so this isn't a choice you're locked into. You can even use both in one project. But they work differently, and the differences show up quickly in day-to-day development. This guide compares how each one is set up, what queries look like, how they handle references, Portable Text and typing, and which situations favour each.

The Short Version

GROQGraphQL
SetupAvailable immediatelyRun sanity graphql deploy
Schema changesPicked up automaticallyRedeploy the API
Query styleFilter, project, compute freelyFields defined by a generated schema
Joins-> and references() in any queryNested fields for references
Reverse lookupsSub-queries with references()Filter with references on a document
Computed fieldsFunctions, conditionals, sub-queriesNot in the query; do it in code
Portable TextReturned as data, references resolvableExposed as raw JSON fields
MutationsSeparate Mutation APINot exposed; use the Mutation API
Type generationSanity TypeGenGraphQL codegen tools
Sanity toolingFirst-classLimited

How Each One Works

GROQ

GROQ (Graph-Relational Object Queries) is Sanity's own query language. You send a query string to the Content Lake and get JSON back, shaped however the query describes:

*[_type == "post" && slug.current == $slug][0] {
  title,
  "slug": slug.current,
  author->{ name },
  "categories": categories[]->title
}

There's no schema to deploy. GROQ queries the documents as stored, so a new field you add to the Studio is queryable the moment an editor fills it in. If GROQ is new to you, start with what GROQ is.

GraphQL

Sanity's GraphQL API is generated from your Studio schema. You deploy it from your Studio project:

npx sanity graphql deploy

The CLI reads the schema, generates GraphQL types, and publishes an endpoint at a URL like this:

https://<projectId>.api.sanity.io/v2025-02-19/graphql/<dataset>/default

Use apicdn.sanity.io instead of api.sanity.io for cached responses. The equivalent query looks like this:

query PostBySlug($slug: String!) {
  allPost(where: { slug: { current: { eq: $slug } } }, limit: 1) {
    title
    slug {
      current
    }
    author {
      name
    }
    categories {
      title
    }
  }
}

The generated schema gives you allPost for lists and Post(id:) for a single document, with where, sort, limit and offset arguments.

Setup and Maintenance

This is the most practical difference.

With GROQ, there's nothing to set up beyond a client:

// src/sanity/client.ts
import { createClient } from 'next-sanity'

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: '2025-02-19',
  useCdn: true,
})

With GraphQL, changing the local schema doesn't change the API. You need to run sanity graphql deploy again after every schema change, usually as a step in CI. The CLI warns about breaking changes before deploying, and --dry-run lets you check without publishing. You can also define several GraphQL APIs, for different workspaces or tags, in sanity.cli.ts.

The schema also has to be GraphQL-compatible. Inline object types used in several places may need to be lifted into named, top-level types so they can become GraphQL types, which can mean refactoring a schema that works perfectly well in the Studio.

Query Power

GROQ is more expressive, because the query decides the shape of the result rather than a fixed schema.

Computed Values

GROQ can compute values in the query:

*[_type == "post"] {
  title,
  "wordCount": length(string::split(pt::text(body), " ")),
  "commentCount": count(*[_type == "comment" && references(^._id)]),
  "label": select(featured => "Featured", "Standard")
}

GraphQL returns fields as the schema defines them. Counts, fallbacks and labels like these have to be calculated in your application code, or stored on the document.

Reverse Lookups

In GROQ, fetching an author with their latest posts is one query:

*[_type == "author" && slug.current == $slug][0] {
  name,
  "posts": *[_type == "post" && references(^._id)] | order(publishedAt desc) [0...5] {
    title, "slug": slug.current
  }
}

Sanity's GraphQL API supports a references filter at document level, so you can find posts that reference an author, but you'll typically do it as a separate root field in the same request and connect the two in code:

query AuthorPage($slug: String!, $authorId: ID!) {
  allAuthor(where: { slug: { current: { eq: $slug } } }, limit: 1) {
    name
  }
  allPost(where: { _: { references: $authorId } }, sort: [{ publishedAt: DESC }], limit: 5) {
    title
    slug { current }
  }
}

Notice you need the author's ID up front, which often means an extra round trip. The dereferencing guide covers the GROQ side in depth.

Filtering Arrays

GraphQL filters on Sanity don't support filtering array fields. GROQ filters arrays anywhere, including inside projections:

*[_type == "product" && "red" in variants[].colour] {
  title,
  "inStock": variants[stock > 0] { sku, colour }
}

Portable Text

Rich text is where the gap is widest. In GraphQL, a Portable Text field called body is exposed as bodyRaw, a JSON scalar with the blocks as stored. References inside it, such as internal links or embedded documents, aren't resolved by default, and the generated types for the content are incomplete.

In GROQ, Portable Text is just an array you can project and dereference like any other:

*[_type == "post" && slug.current == $slug][0] {
  body[] {
    ...,
    markDefs[] {
      ...,
      _type == "internalLink" => { "slug": @.reference->slug.current }
    }
  }
}

That resolves internal link annotations to slugs in the same request, ready for rendering Portable Text in React.

Typing and Developer Experience

Both have good TypeScript stories, just different ones.

GROQ with TypeGen: Sanity TypeGen extracts your schema and generates a type for every query defined with defineQuery:

npx sanity schema extract
npx sanity typegen generate
import { defineQuery } from 'next-sanity'

export const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]{ title, "slug": slug.current }`)
// client.fetch(POST_QUERY, { slug }) is typed automatically

GraphQL with codegen: GraphQL's introspection means the wider ecosystem of GraphQL tools works with Sanity's endpoint, including code generators, IDE plugins and clients like Apollo or urql. The schema is self-documenting, which some teams value highly. Note that the deployed GraphQL schema is publicly introspectable, even if the dataset is private.

Tooling and Ecosystem

Sanity's newer developer tooling is built around GROQ:

  • next-sanity: the client, defineQuery, and the Live Content API helpers expect GROQ.
  • Vision: the query tool in the Studio runs GROQ, and is the fastest way to test a query. See debugging GROQ queries with Vision.
  • Listeners and webhooks: real-time listeners and webhook filters and projections use GROQ.
  • Visual Editing and previews: the Presentation tool and preview helpers in next-sanity are designed for GROQ queries.

GraphQL gets you the broader GraphQL ecosystem instead: Apollo Client caching, federation gateways, and familiar patterns for developers who already know GraphQL.

Drafts and Perspectives

Both APIs support perspectives. GROQ clients set them in the client config or per request:

const previewClient = client.withConfig({
  perspective: 'drafts',
  useCdn: false,
  token: process.env.SANITY_API_READ_TOKEN,
})

The GraphQL API accepts a perspective query parameter on the endpoint URL, with published as the default and options including raw and drafts. So both can serve previews, though the surrounding preview tooling is more mature on the GROQ side.

When to Choose GROQ

  • New projects: there's no deploy step, no schema compatibility work and full access to Sanity's tooling.
  • Content-heavy pages: one query can assemble a page, including references, reverse lookups and Portable Text.
  • Next.js, Astro, Remix and similar frameworks: official integrations use GROQ. The post on using a headless CMS with Astro shows the loader pattern, which works with GROQ queries too.
  • Rapidly changing schemas: changes are queryable immediately.

When to Choose GraphQL

  • An existing GraphQL front end: if your app already uses Apollo or Relay against other services, keeping one query language may outweigh GROQ's advantages.
  • A federated or stitched API: if Sanity is one of several sources behind a GraphQL gateway.
  • Team familiarity: if your developers know GraphQL well and the content model is simple.
  • Strict, introspectable contracts: some teams prefer a deployed schema that changes only on purpose.

Using Both

You don't have to pick one exclusively. A common setup is GraphQL for an existing app that consumes simple content, and GROQ for new pages, previews and anything involving Portable Text. Both read the same documents, so there's no data migration involved in switching later.


FAQ: GROQ vs GraphQL

For most projects, yes. GROQ needs no deployment, handles joins, computed values and Portable Text more flexibly, and is the language Sanity's own tools are built around. GraphQL is a good choice when your stack is already built on it.

Yes. The GraphQL API is generated from your schema at deploy time, so changes in the Studio don't appear until you run sanity graphql deploy again. Many teams add it to their CI pipeline.

Yes. Both query the same Content Lake and dataset, so you can use GraphQL in one part of your stack and GROQ in another without duplicating content.

No. The GraphQL API is read-only. To create, update or delete documents, use the Mutation API, usually through @sanity/client, whichever query language you use for reads.

A Portable Text field is exposed as a raw JSON field with Raw appended to its name, such as bodyRaw. References inside the content aren't resolved by default, so you may need extra queries to resolve internal links or embedded documents.

Not really. Projections will feel familiar, since you list the fields you want in braces. The new parts are filters in square brackets, the arrow operator for references and functions like count() and coalesce(), which most developers pick up in an afternoon.


Conclusion

GROQ and GraphQL both give you access to the same Sanity content, but GROQ is the more capable and lower-maintenance option for most projects. It needs no deploy step, adapts instantly to schema changes, computes and joins data in one request, and is what Sanity's tooling expects.

Choose GraphQL when it fits an existing architecture, such as an Apollo front end or a federated gateway, and build schema deploys into your workflow. Otherwise, start with GROQ, keep the GROQ cheat sheet nearby, and you'll have every query pattern you need.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper