iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Persisted Queries

Persisted operations: pre-register queries on the server; clients send only an ID. Smaller payloads, allowlist security, better caching.

GraphQL — persisted queries

EXAMPLE
// ===== Problem with arbitrary queries =====
// - Public APIs: anyone can craft expensive queries (DoS)
// - Network: queries are large (KBs); ID is tiny
// - Caching: query content varies across clients

// ===== Solution: persisted operations =====
// 1. Extract all client queries at build time
// 2. Register them on the server (or load from a file)
// 3. Client sends only the HASH / ID
// 4. Server looks up + executes
// 5. Optionally REJECT unknown queries (allowlist)

// ===== Build-time extraction =====
// With Apollo Client + persistgraphql-cli:
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset

// codegen.yml
schema: ./schema.graphql
documents: 'src/**/*.{ts,tsx,gql}'
generates:
  ./src/generated/persisted-operations.json:
    plugins:
      - 'persisted-operations'

// Output:
{
  "abcd1234": "query GetUser($id: ID!) { user(id: $id) { name } }",
  "ef56...": "..."
}

// ===== Apollo Client =====
import { ApolloClient, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';

const link = createPersistedQueryLink({ sha256 }).concat(new HttpLink({ uri: '/graphql' }));
const client = new ApolloClient({ link, cache });

// Client sends:
//   POST /graphql { "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "abc..." } } }
// If server doesn't recognise, returns PersistedQueryNotFound; client falls back to sending full query once.

// ===== Server side (Apollo Server) =====
import { ApolloServer, BaseContext } from '@apollo/server';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: { cache: new InMemoryLRUCache({ maxSize: 10_000 }) },
});

// Strict mode (allowlist; reject unknown queries):
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const knownQueries = require('./persisted-operations.json');

// Custom plugin:
plugins: [
  {
    async requestDidStart({ request }) {
      const id = request.extensions?.persistedQuery?.sha256Hash;
      if (!id || !knownQueries[id]) {
        throw new Error('Unknown persisted query');
      }
      request.query = knownQueries[id];
    },
  },
],

// ===== Yoga =====
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations';

const yoga = createYoga({
  schema,
  plugins: [
    usePersistedOperations({
      getPersistedOperation: (key) => knownQueries[key],
    }),
  ],
});

// ===== Benefits =====
// - Smaller request payloads
// - Allowlist mode prevents arbitrary queries from clients
// - Better edge cache (queries identified by hash)
// - Easier query analysis (server knows ALL queries upfront)

// ===== Trade-offs =====
// - Build-time tooling required
// - Schema changes need rebuild + redeploy
// - Harder ad-hoc debugging (no /graphql sandbox in prod)

// ===== Patterns =====
// - Use Automatic Persisted Queries (APQ) for internal / mobile
// - Use STRICT MODE allowlist for public APIs
// - Keep manifest in source control or fetch from CDN
// - Pair with CDN edge caching for read-heavy queries

// ===== Pitfalls =====
// - Forgetting to update the manifest on schema change
// - GET requests with persisted IDs (queryable in browser cache)
// - Mixing arbitrary queries + strict mode without a clear gate

Why it matters

Persisted queries replace large query payloads with a tiny ID. APQ for convenience, strict allowlist for public APIs. Smaller payloads, edge-cache friendly, and a hard guard against arbitrary queries. Build-time codegen + cache-backed server lookup.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Send a hash instead of the full query string. Server resolves the hash.
Try it Yourself »

Discussion

Loading…