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:

  1. 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.
  2. The client re-sends with the full query string plus the hash. The server stores the query, executes it, and returns results.
  3. 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:

  1. 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.

  1. 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.

  1. 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:

  1. Audit mode: Log all unregistered queries, allow all traffic
  2. Soft enforcement: Block unregistered queries from non-production clients only
  3. 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.

I'm a solo developer in Vietnam. 50K Chrome extension users. $500K+ on Upwork. 5 Claude Max subscriptions running agent fleets in parallel. These are my actual CLAUDE.md templates, orchestration configs, and prompts. Not a course. Not theory. The files I copy into every project before I write a line of code. **[See what's inside →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-default&utm_campaign=claude-code-for-graphql-persisted-queries-workflow)** $99 once. Free forever. 47/500 founding spots left.

Related Reading

Built by theluckystrike. More at zovo.one

Find the right skill → Browse 155+ skills in our Skill Finder.