Персистентные запросы GraphQL (Persisted Queries)
Persisted Queries — техника, при которой клиент отправляет хеш запроса вместо полного тела. Сервер кеширует запросы по хешу. Преимущества: меньше трафика (особенно на mobile), возможность GET-запросов для кеширования на CDN, защита от произвольных запросов в продакшен.
Как работает Automatic Persisted Queries (APQ)
Клиент Сервер CDN/Cache
│ │ │
│ POST {hash} │ │
│───────────────>│ │
│ 404 Not Found │ │
│<───────────────│ │
│ │ │
│ POST {hash + query body} │
│───────────────>│ │
│ {data} [сохранить hash→query] │
│<───────────────│ │
│ │ │
│ GET ?hash=... │ │
│──────────────────────────────> │
│ {data} из кеша │
│<────────────────────────────── │
Apollo Client: настройка APQ
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries'
import { generatePersistedQueryIdsFromManifest } from '@apollo/persisted-query-lists'
import { sha256 } from 'crypto-hash'
// Подход 1: Automatic Persisted Queries (fallback при промахе кеша)
const persistedQueryLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true // GET запросы для CDN кеширования
})
const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql'
})
const client = new ApolloClient({
link: persistedQueryLink.concat(httpLink),
cache: new InMemoryCache()
})
Серверная поддержка APQ
// Apollo Server встроенная поддержка APQ
import { ApolloServer } from '@apollo/server'
import { createClient } from 'redis'
import { BaseRedisCache } from 'apollo-server-cache-redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
const server = new ApolloServer({
typeDefs,
resolvers,
// APQ cache: по умолчанию in-memory, в продакшен — Redis
cache: new BaseRedisCache({
client: redis,
ttl: 86400 // 24 часа
}),
// Включить APQ (по умолчанию включено)
persistedQueries: {
ttl: 86400 // Время жизни в cache
}
})
Registered Persisted Queries (более безопасный подход)
APQ позволяет выполнять любой запрос, зарегистрировав его. Registered PQ разрешает только заранее известные запросы:
# Генерация манифеста из клиентских операций (при сборке)
npx generate-persisted-query-manifest \
--documents "src/**/*.graphql" \
--output persisted-query-manifest.json
// persisted-query-manifest.json
{
"format": "apollo-persisted-query-manifest",
"version": 1,
"operations": [
{
"id": "dc67510fb4289672bea757e862d6b00e...",
"name": "GetPosts",
"type": "query",
"body": "query GetPosts($limit: Int) { posts(first: $limit) { ... } }"
},
{
"id": "4a1250de93ad972168776be6ccd86fec...",
"name": "CreatePost",
"type": "mutation",
"body": "mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id } }"
}
]
}
// Сервер: разрешать только зарегистрированные запросы
import manifest from './persisted-query-manifest.json'
const allowedQueries = new Map(
manifest.operations.map(op => [op.id, op.body])
)
// Middleware до ApolloServer
app.use('/graphql', (req, res, next) => {
// В продакшен — только persisted queries
if (process.env.NODE_ENV === 'production') {
const queryId = req.body?.extensions?.persistedQuery?.sha256Hash
|| req.query?.extensions?.persistedQuery?.sha256Hash
if (!queryId) {
return res.status(400).json({
errors: [{ message: 'Only persisted queries allowed in production' }]
})
}
if (!allowedQueries.has(queryId)) {
return res.status(400).json({
errors: [{ message: `Unknown query ID: ${queryId}` }]
})
}
// Подставить тело запроса из манифеста
if (!req.body.query) {
req.body.query = allowedQueries.get(queryId)
}
}
next()
})
CDN кеширование через GET
// Apollo Client: GET для query, POST для mutation
const persistedQueryLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true
})
// Результирующий URL:
// GET /graphql?operationName=GetPosts&variables={"limit":20}&extensions={"persistedQuery":{"version":1,"sha256Hash":"dc67510..."}}
Nginx конфигурация для кеширования GET-запросов:
proxy_cache_path /var/cache/nginx/graphql
levels=1:2 keys_zone=graphql:10m max_size=100m
inactive=1h use_temp_path=off;
location /graphql {
# Кешировать только GET (persisted queries)
if ($request_method = GET) {
proxy_cache graphql;
proxy_cache_key "$uri$is_args$args";
proxy_cache_valid 200 5m;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
}
proxy_pass http://api_backend;
}
Клиент: предзагрузка манифеста
// При использовании Registered PQ в Apollo Client 3.8+
import { generatePersistedQueryIdsFromManifest } from '@apollo/persisted-query-lists'
import manifest from './persisted-query-manifest.json'
const generateLink = createPersistedQueryLink({
generateQueryPreferredFallback: generatePersistedQueryIdsFromManifest({ manifest }),
useGETForHashedQueries: true
})
Мониторинг попаданий в кеш
// Prometheus метрика: cache hit rate для persisted queries
const persistedQueryHits = new Counter({
name: 'graphql_persisted_query_hits_total',
help: 'Persisted query cache hits',
labelNames: ['status'] // hit, miss, not_found
})
// В плагине ApolloServer
const plugin = {
async requestDidStart() {
return {
async executionDidStart({ request }) {
const pq = request.extensions?.persistedQuery
if (pq) {
const cached = await cache.get(`apq:${pq.sha256Hash}`)
persistedQueryHits.labels(cached ? 'hit' : 'miss').inc()
}
}
}
}
}
Срок выполнения
Настройка Automatic Persisted Queries с Redis-кешем и CDN-кешированием через GET — 1–2 рабочих дня.







