Scaling with Confidence: Next Generation Query Building with GROQD and Sanity
Sanity is a headless CMS platform that offers the core features you’d expect from a headless CMS, along with a degree of developer flexibility you don’t see in many other CMSs.
Sanity Studio is Sanity’s CMS interface and is a piece of open source software. Sanity Studio provides a reasonable CMS interface out of the box and offers developers many different ways to tap into and extend the CMS interface. For example, developers can:
- define their own content schemas and types via code;
- create their own preview components to preview data types;
- create their own input components to make it easier for content authors to input specific types of content;
- customize the navigation flow of the entire CMS to meet their content authors’ needs.
The flexibility of Sanity Studio (and Sanity’s APIs in general) makes Sanity a compelling choice for a headless CMS, as it allows developers to quickly tailor the CMS experience to their content authors’ needs.
The Content Lake and GROQ
Part of Sanity’s flexibility comes from the way that content/data is stored. On the backend, Sanity stores all CMS data in a data storage mechanism referred to as the “Content Lake”. A productively naive view of the Content Lake is that it’s a NoSQL document store – where each “document” in Sanity is stored as a JSON document record in the Content Lake. Documents are not grouped together in any sort of collections or tables.
Years ago, Sanity put forth a new query specification GROQ (Graph Relational Object Queries) for querying JSON documents, and GROQ is the primary mechanism for querying data from Sanity’s Content Lake. To learn more about GROQ, check out Sanity’s documentation on it.
The Content Lake is fundamentally unstructured, and enforces no schema upon your CMS data. GROQ can be used to query for this unstructured CMS data, but by the unstructured nature of this CMS data – GROQ query responses are fundamentally “unstructured” as well (in the sense that you can’t “know” what the shape of the response will be).
For example, the query *[_type == "pokemon"]{ name }
might return data in the shape { name: string }[]
or { name: { _ref: string } }[]
or something else entirely – there’s not a way to know the type of the name
field (even if you have a pretty good idea based on your schema configuration). Fundamentally, this means that if you make a GROQ fetch, your response is of type unknown
.
Type-Safe Responses with Zod
As something of a “type nerd”, I believe that if you’re sourcing data from somewhere other than your own code, you should be validating it before assigning it a type. That is, the following code is a smell:
const myData = (await fetch("/president-names").then((res) => res.json())) as { name: string; }[];
Declaring a fetch response with TypeScript’s as
is a bit risky (I find nearly all uses of as
a code smell, but that’s a story for another day).
One common pattern for validating external data is to use a validation library like Zod to validate, and drive the type declaration from that validation. For example:
import { z } from "zod"; // Declare your schema const presidentsSchema = z.array( z.object({ name: z.string(), }), ); // Type: () => Promise<{ name: string; }[]>; const getPresidents = (async = () => { const maybePresidents = await fetch("/president-names").then((res) => res.json(), ); // 👇 use schema to validate data return presidentsSchema.parse(maybePresidents); });
This is a common pattern for fetching data in TypeScript codebases, and we were finding ourselves doing this very thing with fetching Sanity data with GROQ. With GROQ, we were writing queries in GROQ and then separately writing Zod schemas – it felt like there was probably a more streamlined way. Introducing: GROQD.
GROQD in 60 Seconds
GROQD is a “schema-unaware, runtime-safe query builder for GROQ”. Let’s break down what this means:
- Schema-unaware: as I mentioned, Sanity’s Content Lake is unstructured, and even though you can define content schema for your Sanity Studio instance, it provides no guarantees that your data will abide by that schema within the Content Lake. Therefore, GROQD makes no attempt to infer types/schema from your Sanity Studio schema (yet, at least).
- Runtime-safe: GROQD uses Zod under the hood to do runtime validation of query responses.
- Query Builder for GROQ: GROQD is effectively a query builder for GROQ that tries to limit the flexibility of GROQ as much as possible.
The TL;DR with GROQD is: you use the GROQD query builder to craft a query, and it’ll generate the GROQ query string and a Zod schema you can use to validate the GROQ response. Here’s an introductory example (using, of course, Pokemon):
import { q } from "groqd"; // Get all of the Pokemon types, and the Pokemon associated to each type. const { query, schema } = q("*") .filterByType("pokemon") .grab({ name: q.string(), pokemons: q("*") .filter("_type == 'pokemon' && references(^._id)") .grab({ name: q.string() }), }); // Use the schema and the query as you see fit, for example: const response = schema.parse(await sanityClient.fetch(query));
This looks similar to our fetch
+ Zod example above, but with a query builder to build out our GROQ query. Since this is such a common pattern with GROQD, we also provide a little makeSafeQueryRunner
utility to abstract out this fetch + parse process.
import sanityClient from "@sanity/client"; import { q, makeSafeQueryRunner } from "groqd"; const client = sanityClient({ /* ... */ }); // 👇 Safe query runner export const runQuery = makeSafeQueryRunner((query) => client.fetch(query)); // 👇 Now you can run queries and `data` is strongly-typed, and runtime-validated. const data = await runQuery( q("*") .filterByType("pokemon") .grab({ name: q.string(), pokemons: q("*") .filter("_type == 'pokemon' && references(^._id)") .grab({ name: q.string() }), }), ); // data: { name: string; pokemons: { name: string; }[] }[]
GROQD provides various query-building methods that match up to GROQ operations (such as slice
, order
, filter
and more) and schema types that are either minimal wrapper around Zod types (like z.string
or z.number
) or custom schemas that map up to Sanity-specific types (like sanity.image
).
Head on over to the documentation to learn more about query building and schema types. We’ve only scratched the surface in this post.
The Playground
Sanity provides a first-party developer tool called GROQ Playground (Vision) that integrates into your Sanity CMS so developers can test out GROQ queries against their actual dataset. This is similar to tools like GraphiQL for GraphQL that allow you to easily craft and test queries against your actual data source.
The GROQ Playground provides such a nice experience that we decided to build out a similar tool for GROQD – GROQD Playground. A little sample is shown below.
We built GROQD Playground so that it can be easily dropped into any Sanity Studio (v3) project by merely running a yarn add groqd-playground
and then dropping a groqdPlaygroundTool()
into your Sanity configuration’s plugins
list.
Since GROQD acts as an abstraction layer on top of GROQ and Zod and is written in TypeScript, our playground experience has a few more complexities than the GROQ Playground. The GROQD Playground has the following features:
- TypeScript editing experience powered by Monaco, the editor engine that powers VS Code. This gives a nice TypeScript experience, with GROQD and Zod types pre-loaded in so you can get a realistic GROQD experience.
- Ability to run your GROQD query against your actual Sanity dataset and show the result in a custom JSON explorer.
- When the GROQ response fails the Zod schema validation, the playground will show the Zod error messages and highlight the specific fields that failed validation.
- Ability to share queries via URL, in case you want to share playground examples with your teammates.
The GIF below showcases some of these features.
The Arcade
Similar to Sanity Playground, Sanity has built a GROQ Arcade that allows you to experiment with GROQ queries against in-memory datasets that you can edit. This is a great way to experiment with GROQ without connecting to a Sanity project. We build a similar experience into the GROQD documentation, called the GROQD Arcade which you can find here. A demonstrative screenshot is shown below.
This “arcade” shares many of the same features as GROQ Arcade with some overlap with the GROQD arcade, including:
- Ability to edit the JSON dataset that queries run against.
- A TypeScript editor to write GROQD queries.
- A custom JSON response viewer that will highlight any parse errors that occur in the response.
- A set of example datasets and queries.
This arcade allows users to explore GROQD before committing to it, and allows us (as authors) to share examples with the community in a convenient, easy-to-use way!
Check it out
If you’re using Sanity and are a sucker for type safety, give GROQD a shot and let us know what you think!