Use Cached Query Templates
Why use cached templates
Section titled โWhy use cached templatesโEvery standard GraphQL request sends the full query text to Graph. For complex queries, this means hundreds of bytes of query syntax on every API call. Saved query templates (also called persisted queries) let you register queries once and reference them by ID, reducing payload size, improving CDN cache hit rates, and preventing arbitrary queries from untrusted clients.
Step 1: Design your query template
Section titled โStep 1: Design your query templateโWrite the query using variables for any dynamic values. Templates must be parameterized โ hardcoded filter values defeat the purpose.
query GetArticleListing(
$locale: Locales = en
$category: String
$limit: Int = 10
$cursor: String
) {
ArticlePage(
locale: $locale
where: {
_metadata: { status: { eq: "Published" } }
Category: { eq: $category }
}
orderBy: { _metadata: { published: DESC } }
limit: $limit
cursor: $cursor
) {
items {
Headline
Author
Summary
Category
_metadata {
published
url { default }
}
}
total
cursor
}
} Step 2: Register the template
Section titled โStep 2: Register the templateโSave the query template through the Graph admin API. Graph returns a unique hash that clients use to reference the template.
curl -X PUT \
https://cg.optimizely.com/api/content/v3/saved-queries \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64(AppKey:Secret)}' \
-d '{
"name": "GetArticleListing",
"query": "query GetArticleListing($locale: Locales = en, $category: String, $limit: Int = 10, $cursor: String) { ArticlePage(locale: $locale, where: { _metadata: { status: { eq: \"Published\" } }, Category: { eq: $category } }, orderBy: { _metadata: { published: DESC } }, limit: $limit, cursor: $cursor) { items { Headline Author Summary Category _metadata { published url { default } } } total cursor } }"
}'
# Response:
# {
# "hash": "abc123def456789",
# "name": "GetArticleListing"
# } Step 3: Use the template from your frontend
Section titled โStep 3: Use the template from your frontendโReplace the full query text with the persisted query hash in your API calls.
const SAVED_QUERY_HASH = 'abc123def456789';
async function getArticles(category, page) {
const response = await fetch(GRAPH_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${SINGLE_KEY}`,
},
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: SAVED_QUERY_HASH,
},
},
variables: {
category: category,
limit: 10,
cursor: page?.cursor || null,
},
}),
});
return response.json();
} curl -X POST \
'https://cg.optimizely.com/content/v2' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {SingleKey}' \
-d '{
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "abc123def456789"
}
},
"variables": {
"category": "Technology",
"limit": 10
}
}' Step 4: Manage templates in your workflow
Section titled โStep 4: Manage templates in your workflowโTreat saved query templates as part of your codebase. Store them alongside your frontend code and register them during deployment.
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const QUERIES_DIR = './src/graphql/queries';
const GRAPH_ADMIN_URL =
'https://cg.optimizely.com/api/content/v3/saved-queries';
async function registerTemplates() {
const files = fs.readdirSync(QUERIES_DIR)
.filter((f) => f.endsWith('.graphql'));
for (const file of files) {
const query = fs.readFileSync(
path.join(QUERIES_DIR, file), 'utf-8'
);
const name = path.basename(file, '.graphql');
const response = await fetch(GRAPH_ADMIN_URL, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${AUTH_TOKEN}`,
},
body: JSON.stringify({ name, query }),
});
const result = await response.json();
console.log(`Registered ${name}: ${result.hash}`);
}
}
registerTemplates(); Step 5: List and manage existing templates
Section titled โStep 5: List and manage existing templatesโQuery the admin API to see all registered templates and remove outdated ones.
# List all saved queries
curl -s \
https://cg.optimizely.com/api/content/v3/saved-queries \
-H 'Authorization: Basic {base64(AppKey:Secret)}'
# Delete a saved query by name
curl -X DELETE \
https://cg.optimizely.com/api/content/v3/saved-queries/GetArticleListing \
-H 'Authorization: Basic {base64(AppKey:Secret)}' Step 6: Use GET requests for CDN caching
Section titled โStep 6: Use GET requests for CDN cachingโSaved queries can be invoked via HTTP GET, making them cache-friendly at the CDN layer. This is the fastest delivery pattern for public content.
# Invoke a saved query via GET for CDN caching
curl -G 'https://cg.optimizely.com/content/v2' \
--data-urlencode 'extensions={"persistedQuery":{"version":1,"sha256Hash":"abc123def456789"}}' \
--data-urlencode 'variables={"category":"Technology","limit":10}' \
-H 'Authorization: Bearer {SingleKey}' GET requests produce deterministic URLs that CDN edge nodes cache efficiently. Use this approach for high-traffic listing pages and landing pages.
Troubleshooting
Section titled โTroubleshootingโ| Issue | Cause | Resolution |
|---|---|---|
PersistedQueryNotFound error | Template not registered | Register the template via the admin API before using it |
| Stale results after content update | CDN cache not invalidated | Wait for Graph webhooks to trigger cache purge |
| Variables not applying | Malformed JSON in variables | Validate the variables JSON structure matches the query parameters |
| Hash mismatch | Query text changed after registration | Re-register the template and update the hash in your frontend |
Best practices
Section titled โBest practicesโ- Use variables, not string concatenation โ Always parameterize dynamic values. Concatenating values into query strings defeats caching.
- Register templates in CI/CD โ Automate template registration as part of your deployment pipeline to keep templates in sync with your frontend code.
- Version template names โ When making breaking changes to a template, create a new versioned name (e.g.,
GetArticleListing_v2) and deploy the new frontend before removing the old template. - Audit unused templates โ Periodically review registered templates and remove those no longer referenced by any deployed application.
- Prefer GET for public content โ Use GET-based invocations for SingleKey-authenticated queries to maximize CDN cache hit rates.