Dynamic GROQ Query Construction Patterns in JavaScript

5 replies
Last updated: Dec 1, 2025
What’s the best way to construct GROQ query dynamically in JavaScript? Is there a sort of way to construct it in objects or any config which will then get translated to a string query we can use?
I’m unsure whether what I’m doing right now is how it should be. As an example, I defined variables for like:


const baseQuery = (filters) => `*[ ${filters} ]`
const filters = `_type == "products" and name match ${nameKeyword}`
const query = `baseQuery(filters)`
This can go out of hand if I add in projections, additional filters, etc.
AI Update

Your approach with string concatenation can definitely get unwieldy, and you're smart to be thinking about better patterns! The good news is that GROQ parameters are the official, recommended way to handle dynamic values in queries, and they solve many of the issues you're experiencing.

Instead of building queries with JavaScript template strings, use GROQ parameters with the $ syntax. This is the native way to pass dynamic values into GROQ queries:

// ✅ Recommended: Use GROQ parameters
const query = `*[_type == "products" && name match $nameKeyword]`
const params = { nameKeyword: "searchTerm" }

const results = await sanityClient.fetch(query, params)

Why this is better than string concatenation:

  1. Security: Parameters are JSON literals, so they can't be used for GROQ injection attacks
  2. Automatic escaping: The client handles special characters like quotes properly
  3. Type safety: Values maintain their data types
  4. Cleaner code: No messy string interpolation

Building Complex Queries

For queries with multiple filters, projections, and ordering, you can still use parameters while keeping the query structure readable:

const query = `
  *[
    _type == $docType 
    && name match $nameKeyword
    && price > $minPrice
  ] | order($orderField desc) {
    _id,
    name,
    price,
    "category": category->name
  }
`

const params = {
  docType: "products",
  nameKeyword: "laptop*",
  minPrice: 100,
  orderField: "price"
}

const results = await sanityClient.fetch(query, params)

Handling Optional Filters

For conditional filters (like optional search terms), you can use GROQ's select() function or build the query conditionally in JavaScript:

// Approach 1: Conditional query building
const query = nameKeyword 
  ? `*[_type == "products" && name match $nameKeyword]`
  : `*[_type == "products"]`

const params = nameKeyword ? { nameKeyword } : {}

// Approach 2: Using GROQ select()
const query = `
  *[
    _type == "products"
    && select(
      defined($nameKeyword) => name match $nameKeyword,
      true
    )
  ]
`
const params = { nameKeyword: nameKeyword || null }

Note that if you declare a parameter in a query that might be missing, it must be present in the supplied parameters as null (not undefined).

Helper Functions for Query Structure

For truly complex queries, you can create helper functions that return query strings, but still use parameters for values:

const buildProductQuery = (options) => {
  const filters = ['_type == "products"']
  
  if (options.includeNameFilter) {
    filters.push('name match $nameKeyword')
  }
  
  if (options.includePriceFilter) {
    filters.push('price > $minPrice')
  }
  
  const projection = options.detailed 
    ? '{ _id, name, price, description, "category": category->name }'
    : '{ _id, name, price }'
  
  return `*[${filters.join(' && ')}]${projection}`
}

// Usage
const query = buildProductQuery({ 
  includeNameFilter: true, 
  detailed: true 
})
const params = { nameKeyword: "laptop*" }

TypeScript and Type Safety

If you're using TypeScript and want end-to-end type safety, consider using Sanity's native TypeGen with the defineQuery helper:

import { defineQuery } from 'next-sanity'

const query = defineQuery(`*[_type == "products" && name match $nameKeyword]`)
const results = await sanityClient.fetch(query, { nameKeyword: "laptop*" })
// results will be properly typed!

There's also groqd, a community library from FormidableLabs that provides a query builder approach with Zod-like runtime validation if you prefer building queries with a more object-oriented API rather than strings.

Bottom Line

Your instinct is right—string concatenation doesn't scale well. GROQ parameters are the official solution and handle 90% of dynamic query needs. For the remaining complexity around query structure (projections, optional filters, etc.), use helper functions that return parameterized query strings. This keeps your code maintainable while leveraging GROQ's built-in safety features.

Show original thread
5 replies

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?