Clean your GROQ: fragment-based queries that scale
GROQ starts elegant and rots into unreadable sprawl. Here is the fragment-based architecture we use to keep queries typed, cacheable and calm.
GROQ is one of the nicest things about working with Sanity. It is precise, expressive, and lets you fetch exactly the shape your component needs. But like any query language, it degrades the moment a project scales. What began as a tidy data language becomes a dumping ground for one giant, deeply nested, unreadable query that nobody wants to touch.
The fix is not cleverness. It is composition. Break queries into small fragments, compose them upward, and let each fragment own a single concern.
Complexity starts with scaling
A single page query that inlines image resolution, link resolution, author data and eight page-builder blocks quickly hits forty-plus lines of nesting. You lose the ability to reason about any one part of it, and every change risks breaking something three levels down.
Build in four layers
Think of your query file as four layers, from smallest to largest.
Atomic fragments handle one thing. In Turbo Start Sanity the image fragment resolves the asset ref, the LQIP preview and an alt-text fallback chain in one place:
Composite fragments combine atomics. Custom link resolution and mark definitions compose into a rich-text fragment that knows how to expand both blocks and inline images:
Block-level fragments are complete projections for a page-builder block, each co-located with the block it serves. Page-level queries compose everything into a single defineQuery export.
Push logic into the query
GROQ has select, coalesce and array::compact. Use them. Resolving a link's href with select in the query means the frontend never runs a switch statement to decide where a button points. Conditional projections (_type == "hero" => { ... }) let a single page-builder query return the right shape per block, so your React layer just renders what it is handed.
The rules that keep it clean
- Type every query with
defineQueryfromnext-sanityso TypeGen stays in lockstep and a rename breaks the build, not production. - Avoid unbounded spreads in production projections. List the fields you actually need; it keeps the payload small and the query predictable.
- Always pass runtime values as
$params, never string interpolation. It is safer and it lets the CDN cache identical queries. - Name consistently:
somethingFragmentfor fragments,querySomethingDatafor top-level exports.
Why it pays off
Explicit projections stop overfetching, so responses shrink. Stable, reused fragments cache cleanly at the CDN. New contributors read a hierarchy instead of a wall of brackets. And because each block owns its own fragment next to its component, the query and the thing that consumes it never drift apart.
Start atomic, compose upward, and your GROQ stays something you actually want to open.