Claude Code for GraphQL Persisted (2026)
GraphQL persisted queries represent a powerful optimization technique that transforms how your API handles client requests. By pre-registering queries on your server and referencing them by ID instead of sending full query strings, you dramatically reduce payload sizes, improve security, and enhance performance. But managing persisted queries at scale introduces new challenges, versioning, synchronization, and maintaining consistency across environments. This is where Claude Code becomes an invaluable part of your development workflow.
Understanding GraphQL Persisted Queries
When a client sends a traditional GraphQL request, the entire query string travels with every request. For complex queries spanning hundreds of lines, this creates unnecessary network overhead. Persisted queries solve this by storing queries on the server and assigning each a unique identifier. Clients then send only the operation name or hash ID, dramatically reducing request payload.
The benefits extend beyond network optimization:
- Security: Your server only executes pre-registered queries, preventing arbitrary query injection attacks
- Performance: Server-side query parsing and validation happens once at registration time
- Caching: CDNs and proxies can cache responses more effectively with stable request identifiers
- Schema evolution: Breaking changes become easier to track when you know exactly which queries depend on specific fields
How Persisted Queries Work Under the Hood
The standard flow for Automatic Persisted Queries (APQ) follows a two-phase request pattern:
- The client sends a request with only the query hash. The server responds with a “PersistedQueryNotFound” error if it has not seen this hash before.
- The client re-sends with the full query string plus the hash. The server stores the query, executes it, and returns results.
- On all subsequent requests, only the hash is sent.
This means the first request for any new query takes two round-trips, but every request after that is lean. For high-traffic APIs where the same queries run thousands of times per minute, the bandwidth savings compound quickly.
Persisted Queries vs. Automatic Persisted Queries
These two terms are often used interchangeably but have a meaningful distinction:
| Feature | Persisted Queries | Automatic Persisted Queries (APQ) |
|---|---|---|
| Registration | Manual, pre-deployment | Automatic, first-request |
| Security | Stronger. unknown queries always rejected | Weaker. any query auto-registers |
| Build requirement | Requires build step | No build step needed |
| Server cold start | All queries pre-loaded | Queries accumulate at runtime |
| Best for | Production APIs, security-critical apps | Development, rapid iteration |
| Apollo support | Yes (via operation manifest) | Yes (built-in plugin) |
| Urql support | Yes | Yes |
For production APIs serving mobile clients, true persisted queries (manual registration) are almost always the right choice. APQ is a reasonable middle ground for internal tooling or early-stage products.
Setting Up Your Claude Code Workflow
Claude Code excels at orchestrating the complex lifecycle of persisted query management. Here’s how to structure your workflow:
- Define Your Query Registry
Create a centralized location for your persisted queries:
mkdir -p graphql/persisted-queries
Each query lives in its own file with a descriptive name:
graphql/persisted-queries/user-dashboard.graphql
query UserDashboard($userId: ID!) {
user(id: $userId) {
id
name
email
avatarUrl
}
recentOrders(limit: 5) {
id
total
status
createdAt
}
}
Keep your query files focused. One operation per file is the right default. This makes diffs readable, makes hash changes traceable, and makes it easy to retire queries without side effects.
- Generate Query Hashes Automatically
Create a Claude skill that automatically generates the hash identifiers your server expects:
Generate hash using SHA256
echo -n "query UserDashboard..." | shasum -a 256 | cut -d' ' -f1
For Apollo Server, use their built-in CLI:
npx @apollo/persisted-query-lifecyle@latest --generate
You can also write a small Node.js script that processes all query files and outputs a manifest:
// scripts/generate-manifest.js
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { parse, print } = require('graphql');
const queryDir = path.join(__dirname, '../graphql/persisted-queries');
const manifestPath = path.join(__dirname, '../graphql/persisted-manifest.json');
function normalizeQuery(queryString) {
// Parse and re-print to normalize whitespace and formatting
const ast = parse(queryString);
return print(ast);
}
function generateHash(queryString) {
return crypto.createHash('sha256').update(queryString).digest('hex');
}
const manifest = {};
const files = fs.readdirSync(queryDir).filter(f => f.endsWith('.graphql'));
for (const file of files) {
const raw = fs.readFileSync(path.join(queryDir, file), 'utf-8');
const normalized = normalizeQuery(raw);
const hash = generateHash(normalized);
const name = path.basename(file, '.graphql');
manifest[hash] = { name, query: normalized };
}
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
console.log(`Generated manifest with ${files.length} queries`);
Run this during your build step so the manifest is always fresh before deployment.
- Automate Registration in Your Build Pipeline
Integrate persisted query registration into your deployment process:
In your CI/CD configuration
deploy:
script:
- npm run build
- npx apollo-persisted-scripts register
- npm run deploy:production
For a more complete pipeline with environment-specific registrations:
.github/workflows/deploy.yml
name: Deploy with Persisted Queries
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Generate persisted query manifest
run: node scripts/generate-manifest.js
- name: Validate queries against schema
run: npx apollo graph:check --variant production
- name: Register persisted queries
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
APOLLO_GRAPH_REF: ${{ vars.APOLLO_GRAPH_REF }}
run: npx apollo persisted-queries push --manifest graphql/persisted-manifest.json
- name: Deploy application
run: npm run deploy:production
The key insight here is that the manifest generation and registration happen before the application deploys. If registration fails, the deployment stops. you never end up with an app pointing to unregistered query IDs.
Building a Claude Skill for Query Management
Create a specialized Claude skill that handles persisted query operations:
---
name: graphql-pq
description: Manage GraphQL persisted queries
---
GraphQL Persisted Queries Manager
This skill helps you manage persisted queries in your GraphQL project.
Available Operations
Register New Query
When asked to register a query:
1. Read the query from `graphql/persisted-queries/`
2. Generate its SHA256 hash
3. Update the persisted-queries manifest
4. Document in CHANGELOG-PERSISTED-QUERIES.md
Validate Queries
When asked to validate:
1. Check all queries in `graphql/persisted-queries/`
2. Verify they match the current schema
3. Report any deprecated field usage
4. Suggest optimizations
Sync to Environment
When asked to sync:
1. Read current manifest
2. Compare with target environment
3. Generate migration script
4. Execute with confirmation
Extending the Skill with Deprecation Tracking
A more advanced version of this skill can track deprecated fields across your query set:
Deprecation Report
When asked to check deprecations:
1. Fetch the current schema SDL
2. Extract all fields marked with @deprecated
3. Scan every file in `graphql/persisted-queries/`
4. For each deprecated field found, output:
- The query file name
- The field path
- The deprecation reason
- Suggested replacement field if available
5. Write a DEPRECATION-REPORT.md with findings
This means that instead of discovering breaking changes during deployment, you catch them in code review or in a pre-commit hook.
Practical Workflow Example
Here’s a complete workflow for adding a new feature with persisted queries:
Step 1: Write Your Query
Create graphql/persisted-queries/product-catalog.graphql:
query ProductCatalog(
$category: String!
$sortBy: SortOption
$limit: Int
) {
products(category: $category, sortBy: $sortBy, limit: $limit) {
id
name
price
images {
url
alt
}
variants {
id
sku
price
}
}
categories {
id
name
productCount
}
}
Step 2: Generate and Register
Use your Claude skill to generate the hash and update your manifest:
claude -p graphql-pq register product-catalog
The skill outputs:
Generated hash: a1b2c3d4e5f6...
Updated manifest at graphql/persisted-manifest.json
Added entry to CHANGELOG-PERSISTED-QUERIES.md
Step 3: Update the Client
With the hash known at build time, your client code can reference it directly instead of embedding the full query string:
// Before: full query embedded in client bundle
const PRODUCT_CATALOG_QUERY = gql`
query ProductCatalog($category: String!, $sortBy: SortOption, $limit: Int) {
products(category: $category, sortBy: $sortBy, limit: $limit) {
id
name
price
...
}
}
`;
// After: hash-only reference
const PRODUCT_CATALOG_HASH = 'a1b2c3d4e5f6...';
async function fetchProductCatalog(variables) {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: PRODUCT_CATALOG_HASH,
},
},
variables,
}),
});
return response.json();
}
For Apollo Client, this is handled automatically by the persisted queries link:
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const persistedQueriesLink = createPersistedQueryLink({ sha256 });
const httpLink = new HttpLink({ uri: '/graphql' });
const client = new ApolloClient({
link: persistedQueriesLink.concat(httpLink),
cache: new InMemoryCache(),
});
Step 4: Deploy with Confidence
Your CI pipeline now includes the hash in deployments:
Production deployment includes persisted query manifest
APOLLO_GRAPH_ID=production \
APOLLO_VARIANT=production \
npx apollo service push
Server-Side Setup
The client side only works if your server is configured to accept persisted query IDs. Here is what a minimal setup looks like for Apollo Server 4:
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginPersistedQueries } from '@apollo/server/plugin/persistedQueries';
import { generatePersistedQueryManifestExecutor } from '@apollo/persisted-query-lists';
const manifest = require('./graphql/persisted-manifest.json');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginPersistedQueries({
allowUnpersistedOperations: false, // Reject all non-registered queries
executor: generatePersistedQueryManifestExecutor({ manifest }),
}),
],
});
Setting allowUnpersistedOperations: false is the security-critical line. It means any query not in your pre-registered manifest will be rejected with a 400 error. This closes the door on introspection abuse, query complexity attacks, and data scraping via novel query shapes.
For servers that need to remain open during a migration, set it to true temporarily and use logging to identify unregistered queries before you lock down:
ApolloServerPluginPersistedQueries({
allowUnpersistedOperations: true,
onUnpersistedOperation: (query) => {
console.warn(`Unregistered query executed: ${query.substring(0, 100)}`);
// Send to monitoring/alerting
},
}),
Best Practices and Actionable Advice
Version Your Queries
Always version your persisted queries to enable gradual rollbacks:
graphql/persisted-queries/v2/user-profile.graphql
graphql/persisted-queries/v3/user-profile.graphql
This allows clients to migrate incrementally while maintaining backward compatibility.
A practical versioning strategy: keep the old version registered and deployed for at least two release cycles before deregistering it. Mobile clients especially can lag behind, and you do not want a 400 error hitting users who have not updated the app.
Automate Schema Compatibility Checks
Add a pre-commit hook that validates all persisted queries against your schema:
.git/hooks/pre-commit
npx apollo graphql:check --include 'graphql/persisted-queries/*.graphql'
Or with rover (the newer Apollo CLI):
Validate all .graphql files against your registered schema
rover graph check my-graph@production \
--name=my-graph \
--schema ./schema.graphql \
--query-count-threshold 1
This check runs locally before the code even reaches your CI pipeline, which means developers catch broken queries in seconds rather than minutes.
Document Query Dependencies
Maintain a manifest that tracks which features depend on each persisted query:
{
"queries": {
"user-dashboard": {
"hash": "a1b2c3...",
"added": "2026-03-01",
"dependencies": ["orders-service", "user-service"],
"clientVersions": ["mobile-2.4+", "web-3.0+"]
}
}
}
Extend this with a retired field so you can track when queries were deregistered:
{
"queries": {
"user-dashboard-v1": {
"hash": "oldHash123...",
"added": "2025-10-01",
"retired": "2026-02-15",
"retiredReason": "Replaced by user-dashboard-v2 with orders pagination",
"clientVersions": ["mobile-2.0-2.3", "web-2.x"]
}
}
}
This history is invaluable during incident response. If you see errors spiking on a specific hash, you can immediately look up which client versions were using it.
Monitor Query Usage
Track which persisted queries are actually being used:
// Server-side middleware
app.use('/graphql', (req, res, next) => {
if (req.body.queryId) {
metrics.increment(`pq.${req.body.queryId}.calls`);
}
next();
});
With more granular timing:
app.use('/graphql', async (req, res, next) => {
const queryId = req.body?.extensions?.persistedQuery?.sha256Hash;
if (!queryId) return next();
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
metrics.increment(`pq.${queryId}.calls`);
metrics.histogram(`pq.${queryId}.duration_ms`, duration);
if (res.statusCode >= 400) {
metrics.increment(`pq.${queryId}.errors`);
}
});
next();
});
This monitoring data directly informs which queries are safe to retire, which are hot paths worth caching aggressively, and which are unexpectedly slow.
Use a Staged Rollout Approach
For large teams or high-traffic APIs, roll out persisted query enforcement in stages:
- Audit mode: Log all unregistered queries, allow all traffic
- Soft enforcement: Block unregistered queries from non-production clients only
- Hard enforcement: Reject all unregistered queries in production
This staged approach catches gaps in your registration coverage before they become outages.
Common Pitfalls to Avoid
Forgetting to register queries before deployment. Always run your registration step before deploying to production. Unregistered query IDs result in 400 errors for clients.
Using query strings instead of operation names. Ensure your build process extracts and registers operation names, not full query text. This keeps client requests minimal.
Ignoring query deprecation. When removing fields from your schema, update persisted queries first to catch breaking changes before they affect production clients.
Not normalizing queries before hashing. Two queries that are semantically identical but differ in whitespace will produce different hashes. Always normalize (parse + re-print) before computing the hash, or you will end up with duplicate entries in your manifest.
Registering to the wrong environment. It is easy to accidentally push a development manifest to production. Add environment validation to your registration script:
#!/bin/bash
ENVIRONMENT=${1:-development}
if [ "$ENVIRONMENT" = "production" ] && [ -z "$APOLLO_KEY" ]; then
echo "ERROR: APOLLO_KEY is required for production registration"
exit 1
fi
echo "Registering to $ENVIRONMENT..."
Not pinning the normalization library version. If the version of the GraphQL parser you use to normalize queries changes its output format, all your hashes change. Pin the graphql package version in your build tools and test infrastructure to the same version.
Conclusion
Claude Code transforms persisted query management from a manual, error-prone process into an automated, reliable workflow. By defining query registries, creating management skills, and integrating with your CI/CD pipeline, you gain the performance benefits of persisted queries without the operational overhead. Start small, pick one high-frequency query, register it, measure the improvement, and expand from there.
The key is treating persisted queries as first-class artifacts in your development workflow, versioned and managed alongside your code. With Claude Code handling the automation, your team can focus on building features rather than managing API optimization. As your query registry grows, the investment in a solid workflow pays off in faster deployments, fewer incidents, and a meaningfully smaller attack surface for your GraphQL API.
Try it: Paste your error into our Error Diagnostic for an instant fix.
Related Reading
- Claude Code API Regression Testing Workflow Guide
- Claude Code for GraphQL to REST Migration Guide
- Claude Code GraphQL Client Codegen Guide (2026)
Built by theluckystrike. More at zovo.one
Find the right skill → Browse 155+ skills in our Skill Finder.