# Course: Between GROQ and a hard place
https://www.sanity.io/learn/course/between-groq-and-a-hard-place

Go beyond writing data queries – filter, add functions, reshape and transform the responses. Get familiar with GROQ, the query language for Sanity data, webhooks and roles.

---

## Navigation

**Track:** [Mastering content operations](https://www.sanity.io/learn/track/sanity-developer-essentials) · [View as markdown](https://www.sanity.io/learn/track/sanity-developer-essentials.md)

## Contents

1. [Your new favorite query language](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/your-new-favourite-query-language) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/your-new-favourite-query-language.md)
2. [The Vision Tool](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/the-vision-tool) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/the-vision-tool.md)
3. [Filters and projections](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/filters-and-projections) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/filters-and-projections.md)
4. [Functions, in my queries?](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/functions-in-my-queries) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/functions-in-my-queries.md)
5. [Custom functions](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/custom-functions) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/custom-functions.md)
6. [Joins and subqueries](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/joins-and-subqueries) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/joins-and-subqueries.md)
7. [Chained projections](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/chained-projections) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/chained-projections.md)
8. [Query parameters](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/conditional-parameters) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/conditional-parameters.md)
9. [Handling missing values](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/null-handling-and-prevention) · [markdown](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/null-handling-and-prevention.md)

---

## Lesson 1: Your new favorite query language
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/your-new-favourite-query-language

Why you'll learn (and love) GROQ in this course, and why not GraphQL?

> [Video: Your new favorite query language](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/your-new-favourite-query-language)

Sanity provides two methods to query data: **GROQ** and **GraphQL**. 



> [!TIP]
> Compare [GROQ and GraphQL](https://www.sanity.io/learn/content-lake/what-about-graphql) in the documentation



You'll learn GROQ in this track as it is the preferred method for querying content from Sanity and powers other features like user role permissions and filters in functions.



> [!TIP]
> See [Roles](https://www.sanity.io/learn/user-guides/roles) for how GROQ is used to configure "Content resources"


> [!TIP]
> See [Create a Document Function](https://www.sanity.io/learn/functions/function-quickstart) for how function invocations can be limited by a GROQ filter



## Prerequisites and assumptions



- You have a Sanity Studio with `event`, `artist` and `venue` type documents, just like you created in the [Day one content operations](https://www.sanity.io/learn/course/day-one-with-sanity-studio) course.

- You won’t need to know any GROQ in advance to complete these exercises.


## More resources



Here are some great resources already to learn and experiment more with GROQ:



> [!TIP]
> Visual playground [groq.dev](https://groq.dev/)


> [!TIP]
> Free “Introduction to GROQ Query Language” course on [Egghead.io](https://egghead.io/courses/introduction-to-groq-query-language-6e9c6fc0)



---

## Lesson 2: The Vision Tool
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/the-vision-tool

Vision is a plugin bundled with Sanity Studio that allows you to write GROQ queries against the current Project ID and Dataset.

> [Video: 29/10: Training app updates](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/the-vision-tool)

The `visionTool()` plugin should already listed among your Studio's plugins in `sanity.config.ts` 



![Sanity Studio Vision tool with a query for event documents](https://cdn.sanity.io/images/3do82whm/next/9cc66d7a4cfd166d487c7ef3e03e15cded3b2ebc-2240x1480.png)

- [ ] Confirm the Vision plugin is installed


```typescript:./sanity.config.ts
plugins: [
  // ... all your other plugins
  visionTool()
],
```

Now, you have a playground prepared to run queries against the current dataset. 



## Vision is authenticated



Vision uses your logged-in credentials in the Studio to perform **authenticated requests**. 



Commonly your front end client makes unauthenticated requests of a public dataset to return publicly queryable documents. 



This explains why you may see private documents—like drafts—in Vision and not in your front end.



Let's GROQ.



---

## Lesson 3: Filters and projections
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/filters-and-projections

Most queries begin with one character, and it means “get everything.”

> [Video: Filters and projections](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/filters-and-projections)

In Vision, run the following query:



- [ ] Get everything


```groq
*
```

You’ll be returned every document in the dataset. It’s an impressively *fast* query but not very useful. You could filter this data down on your front end, but why not let GROQ filter for you?



> [!NOTE]
> GROQ can return data other than document queries, for example a number or string. You don't *have* to start queries with `*`


- [ ] Get everything with a filter


```groq
*[]
```

The characters `[]` here is an array filter. This query returns the same results as the previous one. This is because our filter is not actually filtering.



You’re likely familiar with working with JavaScript arrays where `[]` allows us to do an index lookup, and that does work, too



- [ ] Try an array index filter to get the **first** item in "everything."


```groq
*[0]
```

You have access to all the data that precedes it within the filter. So the GROQ we write in between `[]` will filter the results of `*`.



Right now, the most obvious is filtering everything down to just the `event` type documents. They all share the same value in the `_type` field. We can add a filter for just this:



- [ ] Get `event` type documents


```groq
*[_type == "event"]
```

Now we’re seeing fewer documents than "everything," and they all only contain the same type. This is a much more useful query.



> [!NOTE]
> Content Lake is schemaless. You don’t *have* to use `_type` in a filter. 



Let’s get every document with a slug, perhaps to populate a sitemap of every page of a website:



- [ ] Perform a `_type`-free query


```groq
*[slug.current != null]
```

Back on topic. Currently, there’s no limit on how many documents it returns. We can use another filter to change that.



Here you're performing "array slicing" by only returning three documents based on their index position.



- [ ] Get only three documents


```json
*[_type == "event"][0...3]
```

If your array slice uses two periods `..` the right-hand side index will be included in the result. So the below will return four documents instead of three.



- [ ] Notice how the total count changes to include the right side index item if you replace `...` with `..`


```groq
*[_type == "event"][0..3]
```

While the query is fast now, as the project gets more complex and the dataset larger, we should begin to think about optimizing how much data is returned. It is easy to return *everything*; writing more filters and adjusting how much we return can be faster.



The data returned from a document is defined by its “Projection.” A list of field names will be looked up in each document.



- [ ] Return only the `name` and `date` fields from the first three `event` documents


```groq
*[_type == "event"][0...3]{
  name,
  date
}
```

Much better. You shouldn't return data if you don’t need it.



Naming specific attributes inside a projection also clarifies what data your consuming application depends on.



You can return keys that exist on a document **as well as** arbitrarily add new keys to the returned value, perhaps calculated by values in the document itself.



For this project, we know that an `event` is in the past if its `date` field is older than the `now()` function. (More on functions in the next exercise.)



Instead of running logic in our front end to differentiate these, we can add the logic to our query.



- [ ] Add arbitrary `isPast` key


```groq
*[_type == "event"][0...3]{ 
  name,
  date,
  "isPast": date < now()
}
```

One more useful operator to know at this point is how to order documents. That is done with a pipe and the GROQ function `order`.



Take note that the `order()` function has been placed **before** the "array slicing" filter. This ensures you order **all** documents and then return only the top three.



- [ ] Order documents by the date and time they were created


```groq
*[_type == "event"]|order(_createdAt desc)[0...3]{ 
  name,
  date,
  "isPast": date < now()
}
```

GROQ functions are super powerful; let's unpack them in the next exercise.



---

## Lesson 4: Functions, in my queries?
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/functions-in-my-queries

With GROQ you can do more than return values; you can compute and transform them from within the query.

> [Video: Functions, in my queries?](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/functions-in-my-queries)

GROQ offers a [number of Functions](https://www.sanity.io/docs/groq-functions) that you can experiment with. These will save you from needing to "post-process" data in your front end or API to shape data as desired from within the query.



Here are a few simple examples:



- [ ] `count()` all the `event` documents in the dataset


```json
count(*[_type == "event"])
```

- [ ] `coalesce()` takes any number of arguments and returns the first one that isn't `null` – use it to return the `venue` name if defined, otherwise the string "To be confirmed"


```groq
*[_type == "event" && eventType == "in-person"]{
  "venue": coalesce(venue->name, "To be confirmed")
}
```

If you have completed [Handling schema changes confidently](https://www.sanity.io/learn/course/handling-schema-changes-confidently) `eventType` might now be named `format` – adjust the query above accordingly.



- [ ] `defined()` returns `true` for values that are **not** `null` – query for `event` documents that have a `headline` artist defined


```groq
*[_type == "event" && defined(headline)]
```

- [ ] Return an array of unique `headline` `artist` names that have upcoming events


```groq
array::unique(
  *[
    _type == "event" 
    && date > now() 
    && defined(headline)
  ].headline->name
)
```

This is just scratching the surface! There are functions for geolocation, weighted search, the delta between documents as they change in functions – and more!



---

## Lesson 5: Custom functions
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/custom-functions

Reduce code duplication by including custom functions in your GROQ queries.

> [Video: Custom functions](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/custom-functions)

For advanced use cases where you need to regularly perform the same transformation it is also possible to write custom GROQ functions.



This is especially useful for complex schema types where you want to return the same attributes for different fields. For example resolving images or references the same way.



In the example below, both the `headline` and `venue` reference are passed into the same function which will resolve the reference and return the `name` attribute.



- [ ] Resolve artist and venue references with the same custom function


```groq
fn learn::reference($doc) = $doc->{name};

*[_type == "event"]{
  "headline": learn::reference(headline),
  "venue": learn::reference(venue)
}
```

This is a marked improvement from string interpolation with strings and variables in JavaScript and should result in shorter queries overall.



> [!TIP]
> See [GROQ Custom Functions](https://www.sanity.io/docs/specifications/groq-functions#a1a2ddcad176) in the documentation for additional notes and limitations



---

## Lesson 6: Joins and subqueries
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/joins-and-subqueries

When a document contains a reference it only stores the ID of that document, GROQ can resolve it

> [Video: Joins and subqueries](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/joins-and-subqueries)

Try querying for an event document and return the venue reference.



- [ ] Return the `venue` of every `event`


```groq
*[_type == "event" && defined(venue)][0]{ 
  venue
}
```

The venue attribute will return a reference to another document's ID in the attribute `_ref`.



You can “resolve” this reference with this operator `->`



This deceptively simple operator will perform a “sub-query” to resolve the document with that `_id`. 



- [ ] Resolve the `venue` reference


```groq
*[_type == "event" && defined(venue)][0]{ 
  venue->
}
```

Excellent! We now have the actual details of the category document. But again, we have the “too much data” problem. This can be fixed by adding an additional projection.



- [ ] Get only the `name` of the `venue` document


```groq
*[_type == "event" && defined(venue)][0]{ 
  venue->{
    name
  }
}
```

## Reverse reference lookups



It’s possible to create your own subquery anywhere in a document. However, you should be mindful that additional nesting and queries can increase response time and the volume of data. Exercise caution.



References are one-way in that they are stored on a document. However the `references()` GROQ function allows you to look up all other references to a document ID.



A useful sub-query for this project would be to look up every event when querying for an artist, like a reverse lookup of references.



- [ ] Using the `references()` GROQ function, get every `event` featuring the current `artist`.


```groq
*[_type == "artist"][0]{ 
  name,
  "events": *[_type == "event" && references(^._id)]{
    name    
  }
}
```

The `^` character traverses out of the current filter to access the "parent." In the above example, it is accessing the `_id` attribute of the artist document.



> [!TIP]
> See [GROQ operators](https://www.sanity.io/learn/specifications/groq-operators) in the documentation for more details on what is available when writing GROQ queries


> [!TIP]
> See [High Performance GROQ](https://www.sanity.io/docs/high-performance-groq) for more guidance on keeping queries performant



---

## Lesson 7: Chained projections
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/chained-projections

Write sequential projections to transform responses into different shapes

> [Video: Chained projections](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/chained-projections)

So far you have only written a single projection after a each filter. It is possible to chain projections, where each projection only contains the attributes of the previous one.



- [ ] Query for an event with a `headline` and `venue` to return a `title` string for the event


```groq
*[
  _type == "event" 
  && defined(headline)
  && defined(venue)
][0]{
  headline->{name},
  venue->{name}
}{
  "title": headline.name + " at " + venue.name
}
```

You might think about this as using each projection to create a variable which can be used in the next projection.



In the example above we know `headline` and `venue` are defined because that has been added to the filter. 



But the same query can be even more flexible and handle instances where data could be missing.



- [ ] Query for any event and render an appropriate `title` string using `select()`


```groq
*[_type == "event"][0]{
  headline->{name},
  venue->{name}
}{
  "title": select(
    defined(headline.name) && defined(venue.name) => headline.name + " at " + venue.name,
    defined(headline.name) => headline.name,
    defined(venue.name) => venue.name,
    "Untitled event"
  )
}
```

The `select()` GROQ function will return the first item that returns true. In this instance, any event that has both a `headline` and `venue` will return the complete string. Otherwise fall back to either the `headline` name, or `venue` name, or a string "Untitled event."



When you need to use dynamic values in GROQ queries you turn to parameters, let's look at those in the next lesson.



---

## Lesson 8: Query parameters
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/conditional-parameters

Use parameters as variables in your queries, whether they have values or not.

> [Video: Query parameters](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/conditional-parameters)

## Static parameters



The most common use case for using parameters in GROQ for front ends is matching a document against its slug, using a dynamic value from your front end framework.



- [ ] Query for a single event with this matching slug. If you used the seed data in a previous course this document should exist.


```groq:Query
*[
  _type == "event"
  && slug.current == $slug
][0]
```

```json:Parameters
{
  "slug": "the-national-at-the-bowery-ballroom"
}
```

This should return a single document. 



Using parameters in GROQ queries is superior and safer than string concatenation in JavaScript. In the bad example below the variable needs to be wrapped in quotes and can be easily formatted incorrectly.



```javascript
// ❌ Avoid doing this, use params instead!
const EVENT_QUERY = `*[
  _type == "event"
  && slug.current == "${slug}"
][0]`
```

## Conditional parameters



You can use `select` to conditionally use a parameter to filter documents.



- [ ] Query to return the `name` of future events if a date is provided, or all events if not.


```groq:Query
*[
  _type == "event"
  && select(
    defined($date) => date > now(),
    true
  )
].name
```

```json:Parameters
{
  "date": "2025-07-04T06:44:33.014Z"
}
```

Select will return the first item that returns true. So the first condition uses `defined()` to check if `$date` is not `null`



- [ ] Query again without a `date` value


```json:Parameters
{
  "date": null
}
```

Otherwise, it returns `true` which will have no impact on the filter because all documents satisfy the condition.



Note that a parameter cannot be `undefined`, so if you declare a parameter in a query which could be missing, it must be present in the supplied parameters as `null`.



---

## Lesson 9: Handling missing values
https://www.sanity.io/learn/course/between-groq-and-a-hard-place/null-handling-and-prevention

Named attributes in a projection will return null by default, but they don't have to.

> [Video: Handling missing values](https://www.sanity.io/learn/course/between-groq-and-a-hard-place/null-handling-and-prevention)

The following query will only return attributes that exist in the matching document because no attributes have been defined in a projection.



```groq
*[_type == "event"][0]
```

However if you now add a projection and define an attribute which does not exist, instead of being absent from the response, it will return as `null`.



- [ ] **Query** for an attribute that doesn't exist


```groq
*[_type == "event"][0]{
  fakeAttribute
}
```

This is `null` response is expected and in many cases useful—but it can be annoying to handle in your front end.



It's good practice to use projections but now you have to expect every attribute to potentially be `null`, and this requires a lot of defensive coding. 



For example:



```javascript
data?.headline?.name ?? 'Headliner not yet confirmed'
```

With a little extra GROQ you can prevent `null` values and return explicit values instead.



## Always return strings



The `coalesce()` function returns the first value that is not `null`.



- [ ] Query for events which may or may not have a `headline` artist reference and always return a string


```groq
*[_type == "event"][0...5]{
  "headline": coalesce(headline->name, "Unconfirmed")
}
```

You can use this same tactic values other than strings, you can do numbers too, or...



## Always return arrays



Similarly, the `coalesce` function can be used to always return an array—even an empty array—which can be preferable to `null`.



The `details` Portable Text field is an array of block content.



- [ ] Query for events and always return an array for the Portable Text content


```groq
*[_type == "event"][0...5]{
  "details": coalesce(details, [])
}
```

Now instead of checking if `details` is an array *and* if it has blocks, you need only check its length.



## Always return booleans



Setting aside the fact that [I think you should avoid using booleans at all](https://www.youtube.com/watch?v=txH6CRHnhLk)—string literals are type safe and more flexible—if you have a boolean *field* you can make sure it only ever returns a boolean *value*.



Your current schema does not have a boolean field type. But to always return booleans, create a boolean value in the query.



For example, if you had a boolean field called `isSoldOut` and queried it like this:



```groq
*[_type == "event"]{
  isSoldOut
}
```

The resulting `isSoldOut` could be one of `true`, `false` or `null` (the last of which is *falsy*, but still this negates the point of a boolean, right?)



You can always return a boolean by querying for it like this:



```groq
*[_type == "event"]{
  "isSoldOut": isSoldOut == true
}
```

Now `isSoldOut` is only ever `true` or `false`.



## Wrap up



With all that, you're much sharper on all things GROQ! Good luck completing the rest of the [Mastering content operations](https://www.sanity.io/learn/track/sanity-developer-essentials) track



---

## Related Resources

- [Track overview](https://www.sanity.io/learn/track/sanity-developer-essentials.md)
- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
