
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeYour 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:
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)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).
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*" }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.
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 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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store