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...
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.
The Recommended Approach: GROQ Parameters
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:
- Security: Parameters are JSON literals, so they can't be used for GROQ injection attacks
- Automatic escaping: The client handles special characters like quotes properly
- Type safety: Values maintain their data types
- 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.
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.