Persisted Query GraphQL adalah cara praktis untuk memastikan server hanya menerima operasi yang sudah dikenal, alih-alih membiarkan klien mengirim query bebas ke production. Jika dihubungkan ke CI/CD, pendekatan ini bisa mencegah breaking change operasional: schema berubah, tetapi query yang masih dipakai aplikasi diam-diam menjadi invalid dan baru gagal saat runtime.

Masalah yang biasanya muncul bukan hanya kompatibilitas schema. Query liar dari klien, payload request besar karena dokumen GraphQL dikirim berulang, serta sulitnya memetakan operasi aktif di production juga sering menjadi sumber insiden. Dengan persisted query, tim punya daftar operasi yang eksplisit, bisa divalidasi di CI, dan bisa dijadikan kontrak operasional antara klien dan server.

Apa itu persisted query dan kenapa relevan untuk CI

Secara sederhana, persisted query menyimpan dokumen GraphQL di sisi server atau registry, lalu klien cukup mengirim identifier—biasanya hash dari isi query—saat request. Server akan mencari dokumen berdasarkan hash tersebut, lalu mengeksekusinya jika ditemukan.

Pola ini relevan untuk CI karena operasi menjadi artefak yang bisa dilacak dan diuji. Bukan lagi “klien nanti kirim query apa saja”, tetapi “hanya query yang ada di manifest ini yang dianggap valid untuk dijalankan”. Dari sini muncul tiga manfaat operasional utama:

  • Mencegah query liar: server dapat menolak operasi yang tidak ada di allowlist.
  • Mengurangi payload: klien tidak perlu selalu mengirim dokumen query lengkap.
  • Mendeteksi breaking change lebih awal: semua query tersimpan bisa divalidasi terhadap schema terbaru di CI sebelum merge.

Persisted query vs APQ

Istilah ini sering tercampur. Dalam praktik, ada dua pola yang mirip tetapi tujuan operasionalnya berbeda:

  • Persisted query / allowlist: server hanya mengizinkan operasi yang sudah didaftarkan. Fokusnya kontrol, keamanan, dan stabilitas.
  • Automatic Persisted Query (APQ): klien mengirim hash terlebih dahulu untuk efisiensi bandwidth; jika server belum punya dokumennya, klien mengirim query lengkap sebagai fallback. Fokusnya optimasi jaringan.

Jika target Anda adalah mencegah breaking change operasional dan menutup pintu untuk query tak dikenal, gunakan mode allowlist yang ketat. APQ saja tidak cukup, karena fallback query penuh masih memungkinkan operasi baru lolos tanpa review pipeline.

Cara kerja allowlist query berbasis hash

Alur dasarnya biasanya seperti ini:

  1. Tim menulis operasi GraphQL di file yang bisa dilacak di repository, misalnya .graphql.
  2. Saat build atau langkah generate, setiap operasi di-hash secara deterministik, umumnya dari isi dokumen yang sudah dinormalisasi.
  3. Hasilnya dimasukkan ke manifest persisted query, misalnya file JSON berisi pasangan hash -> query atau hash -> nama operasi.
  4. Manifest dipublikasikan ke server, gateway, atau registry yang dipakai runtime.
  5. Klien saat runtime mengirim hash operasi. Server hanya mengeksekusi jika hash tersebut ada di allowlist.

Contoh manifest sederhana:

{
  "d1d4d2d5...": {
    "name": "GetUserProfile",
    "body": "query GetUserProfile($id: ID!) { user(id: $id) { id name avatarUrl } }"
  },
  "a8bc91ef...": {
    "name": "UpdateEmail",
    "body": "mutation UpdateEmail($id: ID!, $email: String!) { updateEmail(id: $id, email: $email) { id email } }"
  }
}

Struktur persisnya bisa berbeda tergantung tool, tetapi prinsipnya sama: ada daftar operasi yang diketahui dan bisa diverifikasi.

Kenapa hash, bukan nama operasi saja?

Nama operasi tidak selalu unik dan tidak menjamin isi query benar-benar sama. Hash dari dokumen lebih kuat sebagai identitas karena berubah jika field, fragment, atau variabel berubah. Ini penting untuk CI karena perubahan kecil pada query tetap tercatat sebagai perubahan kontrak operasional.

Struktur repo yang memudahkan automasi

Agar pipeline sederhana, simpan query sebagai artefak eksplisit di repository. Hindari hanya menaruh query tersembunyi di string inline tanpa proses ekstraksi yang jelas.

repo/
  apps/
    web/
      src/
      graphql/
        queries/
          GetUserProfile.graphql
          ListOrders.graphql
        mutations/
          UpdateEmail.graphql
    mobile/
      src/
      graphql/
        queries/
  packages/
    graphql/
      schema.graphql
      fragments/
        UserFields.graphql
      persisted/
        manifest.json
      scripts/
        generate-persisted-manifest.js
        validate-operations.js
        check-schema-compat.js
  .github/workflows/
    graphql-ci.yml

Pemisahan seperti ini memudahkan beberapa hal:

  • Semua operasi klien bisa dipindai dan divalidasi.
  • Manifest bisa dibangun ulang secara konsisten.
  • Schema dan operasi hidup dekat dengan tool CI yang memeriksanya.
  • Review PR lebih jelas karena perubahan query terlihat sebagai file tersendiri.

Integrasi persisted query dengan Apollo, urql, dan client umum

Detail integrasi bergantung pada stack, tetapi pola besarnya konsisten: ekstrak operasi, generate manifest, kirim hash saat runtime, dan aktifkan enforcement di server.

Apollo Client

Pada ekosistem Apollo, persisted query biasanya diintegrasikan melalui link atau proses build yang menghasilkan mapping operasi ke hash. Jika Anda memakai mode allowlist, pastikan server tidak menerima fallback query penuh untuk operasi yang belum terdaftar.

Prinsip implementasi di sisi klien:

  • Operasi GraphQL didefinisikan secara statis, bukan dibangun dinamis dari input runtime.
  • Build step menghasilkan hash dari seluruh operasi yang dipakai aplikasi.
  • Request runtime mengirim hash operasi, nama operasi, dan variabel.

urql

Di urql, konsepnya serupa. Operasi bisa diproses melalui exchange atau layer build yang menghasilkan identitas persisted query. Jika Anda memiliki infrastruktur sendiri, yang penting bukan nama library-nya, melainkan konsistensi antara dokumen saat build dan hash yang dikirim saat runtime.

Client umum atau fetch manual

Jika Anda tidak memakai client GraphQL besar, persisted query tetap bisa diterapkan. Cukup siapkan mapping lokal dari nama operasi ke hash, lalu kirim payload seperti:

{
  "operationName": "GetUserProfile",
  "variables": { "id": "123" },
  "extensions": {
    "persistedQuery": {
      "sha256Hash": "d1d4d2d5..."
    }
  }
}

Server kemudian memetakan hash ke dokumen query yang sudah terdaftar. Bentuk payload extensions umum digunakan, tetapi format akhir tetap mengikuti implementasi server atau gateway Anda.

Catatan: jika tujuan Anda adalah enforcement, jangan izinkan fallback otomatis ke query penuh di production. Fallback lebih cocok untuk fase transisi atau APQ murni, bukan allowlist ketat.

Validasi schema terhadap query tersimpan di CI

Inilah bagian paling penting untuk mencegah breaking change operasional. Saat schema berubah, CI harus memeriksa semua operasi yang tersimpan dan memblok merge jika ada yang tidak lagi valid.

Apa yang divalidasi

  • Field yang dipakai query masih ada.
  • Argumen field masih kompatibel.
  • Tipe hasil masih memenuhi kontrak yang dipakai klien.
  • Fragment masih cocok dengan tipe target.
  • Variabel operasi masih sesuai dengan definisi schema.

Secara teknis, ini adalah validasi dokumen GraphQL terhadap schema hasil build terbaru. Karena sumber query berasal dari allowlist, Anda tahu tepat operasi mana yang harus dijaga.

Contoh alur script validasi

# 1) lint schema dan dokumen
node packages/graphql/scripts/validate-operations.js

# 2) generate manifest persisted query
node packages/graphql/scripts/generate-persisted-manifest.js

# 3) cek kompatibilitas schema baru terhadap semua operasi tersimpan
node packages/graphql/scripts/check-schema-compat.js

Contoh pseudo-code validasi:

import { buildSchema, parse, validate } from 'graphql';
import fs from 'node:fs';

const schemaSDL = fs.readFileSync('packages/graphql/schema.graphql', 'utf8');
const schema = buildSchema(schemaSDL);

const manifest = JSON.parse(
  fs.readFileSync('packages/graphql/persisted/manifest.json', 'utf8')
);

let hasError = false;

for (const [hash, op] of Object.entries(manifest)) {
  const doc = parse(op.body);
  const errors = validate(schema, doc);

  if (errors.length > 0) {
    hasError = true;
    console.error(`Operation invalid: ${op.name} (${hash})`);
    for (const err of errors) {
      console.error(`- ${err.message}`);
    }
  }
}

if (hasError) {
  process.exit(1);
}

Contoh di atas sengaja generik. Di proyek nyata, Anda bisa memakai tool yang sudah tersedia di ekosistem GraphQL untuk ekstraksi operasi, validasi dokumen, dan pemeriksaan perubahan schema. Yang penting, hasil akhirnya adalah CI gagal jika satu saja operasi tersimpan rusak.

Pipeline CI/CD contoh yang aman

Pipeline berikut cocok untuk tim yang ingin menjadikan persisted query sebagai pagar operasional, bukan sekadar optimasi jaringan.

1. Lint schema dan dokumen

Langkah awal memeriksa SDL schema, naming, fragment duplikat, operasi tanpa nama, dan masalah statis lain. Ini mencegah error sederhana lolos ke tahap berikutnya.

2. Build manifest persisted query

Semua file operasi dipindai, dinormalisasi, lalu di-hash. Hasil manifest bisa dibandingkan dengan yang ada di repository untuk memastikan tidak ada operasi runtime yang belum didaftarkan.

3. Cek kompatibilitas query tersimpan

Schema terbaru divalidasi terhadap seluruh manifest. Jika ada field dihapus, argumen berubah, atau nullability berubah secara tidak kompatibel, merge diblok.

4. Bandingkan perubahan schema

Selain memvalidasi operasi, ada baiknya CI juga memeriksa perubahan schema terhadap baseline branch utama. Tujuannya untuk mendeteksi perubahan yang secara semantik berpotensi breaking meskipun belum menyentuh query saat ini. Contoh: field publik dihapus tetapi belum dipakai oleh aplikasi internal yang ada di monorepo.

5. Publish manifest dan deploy berurutan

Urutan release penting. Jika server lebih dulu enforcement terhadap manifest baru sementara klien lama belum compatible, request bisa gagal. Alur aman umumnya:

  1. Tambah field/schema baru yang dibutuhkan, tetap kompatibel dengan klien lama.
  2. Publish manifest baru ke registry/server.
  3. Deploy klien yang mulai memakai operasi atau field baru.
  4. Setelah semua klien lama aman, baru hapus field lama dan ulangi validasi CI.

Contoh workflow CI sederhana

name: graphql-ci

on:
  pull_request:
  push:
    branches: [main]

jobs:
  validate-graphql:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: node packages/graphql/scripts/validate-operations.js
      - run: node packages/graphql/scripts/generate-persisted-manifest.js
      - run: git diff --exit-code packages/graphql/persisted/manifest.json
      - run: node packages/graphql/scripts/check-schema-compat.js

Poin penting dari contoh di atas:

  • validate-operations memeriksa kualitas dokumen.
  • generate-persisted-manifest memastikan manifest bisa direproduksi dari source query.
  • git diff --exit-code berguna untuk mencegah perubahan query tanpa update manifest.
  • check-schema-compat menjadi penjaga utama breaking change operasional.

Strategi saat schema berubah

Perubahan schema hampir selalu menjadi sumber gesekan antara tim backend dan frontend. Persisted query membuat dampaknya lebih terlihat, tetapi Anda tetap perlu strategi rollout yang aman.

Gunakan pola additive terlebih dahulu

Jika ingin mengubah bentuk data, lebih aman menambah field baru daripada langsung mengganti field lama. Klien dipindahkan bertahap ke field baru, lalu field lama dideprecate dan akhirnya dihapus setelah tidak ada lagi operasi persisted yang memakainya.

Rawat daftar operasi aktif

Manifest sebaiknya tidak menjadi kuburan operasi lama. Tandai operasi yang sudah tidak dipakai dan bersihkan secara berkala. Jika tidak, CI akan terus menjaga kompatibilitas untuk query yang sebenarnya sudah mati, sehingga perubahan schema menjadi terlalu mahal.

Bedakan operasi per aplikasi atau channel release

Untuk mobile, kompatibilitas lebih sensitif karena versi lama bisa hidup berminggu-minggu atau berbulan-bulan. Dalam kasus ini, simpan allowlist per platform atau per release channel. Dengan begitu, backend tahu field mana yang masih harus dipertahankan berdasarkan klien yang benar-benar aktif.

Jangan menghapus field hanya karena tidak ada di branch saat ini

Jika manifest hanya dibangun dari repository utama, Anda bisa kehilangan visibilitas terhadap klien yang dirilis di luar branch itu, misalnya mobile app lama. Solusinya adalah menggabungkan data dari manifest build dan observabilitas produksi, atau menyimpan registry operasi per versi aplikasi.

Trade-off dan keterbatasan

Persisted query bukan tanpa biaya. Beberapa trade-off yang perlu dipahami:

  • Disiplin build meningkat: setiap operasi baru harus masuk pipeline generate manifest.
  • Kurang cocok untuk query benar-benar dinamis: jika aplikasi membangun query dari runtime secara bebas, allowlist menjadi sulit diterapkan.
  • Perlu sinkronisasi release: server, registry, dan klien harus memiliki urutan deploy yang aman.
  • Manifest bisa membesar: pada organisasi besar, daftar operasi lintas aplikasi dapat menjadi artefak yang perlu dikelola dengan baik.
  • Debugging awal lebih kompleks: saat request ditolak, Anda harus menelusuri apakah hash tidak cocok, manifest belum ter-publish, atau schema baru tidak kompatibel.

Meski begitu, untuk sistem yang ingin stabil di production, biaya ini biasanya sepadan karena kegagalan berpindah dari runtime ke CI, tempat error jauh lebih murah diperbaiki.

Kegagalan umum dan cara debug

Hash tidak cocok antara build dan runtime

Penyebab umum: normalisasi dokumen berbeda, fragment digabung dengan urutan yang tidak konsisten, atau proses minify menghasilkan string berbeda. Pastikan hashing dilakukan dari representasi dokumen yang deterministik dan berasal dari pipeline yang sama dengan artefak runtime.

Manifest sudah berubah, tetapi server masih menolak operasi

Biasanya manifest belum benar-benar ter-publish ke environment target, ada cache lama di gateway, atau deploy server dan klien tertukar urutannya. Verifikasi versi manifest yang aktif di server dan cocokkan dengan hash yang dikirim klien.

CI lolos, tetapi production tetap gagal

Sering terjadi jika CI hanya memeriksa query yang ada di monorepo, sementara ada klien lama atau aplikasi lain yang tidak ikut tervalidasi. Solusinya adalah memperluas cakupan manifest atau memakai registry operasi lintas aplikasi.

Query ditolak setelah refactor yang terlihat aman

Refactor fragment sering mengubah dokumen final sehingga hash ikut berubah. Jika manifest tidak diperbarui atau klien masih memakai hash lama, request akan gagal meskipun hasil query semantik sama. Karena itu, treat perubahan hash sebagai perubahan artefak build yang harus dirilis dengan benar.

Contoh konfigurasi sederhana untuk enforcement

Implementasi enforcement bisa dilakukan di gateway atau server GraphQL. Contoh konfigurasi konseptual:

{
  "persistedQueries": {
    "enabled": true,
    "mode": "allowlist",
    "manifestPath": "packages/graphql/persisted/manifest.json",
    "allowUnknownOperations": false
  }
}

Nama kunci konfigurasi nyata akan bergantung pada server yang Anda pakai. Yang perlu dijaga adalah semantiknya:

  • persisted query aktif,
  • mode enforcement jelas,
  • manifest dimuat dari artefak yang terkontrol,
  • operasi tak dikenal ditolak.

Checklist adopsi bertahap

Jika tim belum pernah memakai persisted query, jangan langsung memaksa semua aplikasi sekaligus. Adopsi bertahap lebih aman.

  1. Inventaris operasi: pindahkan query penting ke file statis yang bisa dipindai.
  2. Generate manifest tanpa enforcement: mulai bangun allowlist, tetapi server masih menerima query biasa untuk observasi awal.
  3. Tambahkan validasi CI: blok merge jika query tersimpan tidak valid terhadap schema terbaru.
  4. Nyalakan telemetry: catat operasi yang masih datang sebagai query penuh atau hash yang tidak dikenal.
  5. Aktifkan enforcement per aplikasi: mulai dari web internal atau service yang paling mudah dikontrol.
  6. Atur release order: publish schema kompatibel, lalu manifest, lalu klien.
  7. Bersihkan operasi usang: buat proses berkala untuk menghapus query yang tidak lagi aktif.

Kapan pendekatan ini paling layak dipakai

Persisted query dengan CI paling cocok jika Anda menghadapi salah satu kondisi berikut:

  • banyak klien mengakses satu GraphQL API,
  • insiden sering terjadi karena field/argumen berubah diam-diam,
  • ingin menutup akses ke query ad-hoc di production,
  • butuh payload request lebih kecil,
  • ingin menjadikan operasi GraphQL sebagai kontrak yang bisa direview dan diuji.

Untuk proyek kecil dengan satu klien dan siklus deploy sangat sederhana, manfaatnya mungkin belum terasa penuh. Namun begitu jumlah operasi dan tim bertambah, persisted query yang terhubung ke CI biasanya menjadi alat kontrol yang sangat berguna.

Penutup

Persisted Query GraphQL bukan sekadar optimasi bandwidth. Jika dipasang bersama allowlist dan validasi di CI/CD, ia menjadi mekanisme kontrak operasional: server tahu operasi mana yang sah, tim tahu schema mana yang aman diubah, dan pipeline bisa menghentikan merge sebelum bug sampai ke production.

Mulailah dari inventaris operasi dan validasi schema terhadap query tersimpan. Setelah itu, tambahkan enforcement bertahap dan disiplin release yang benar. Hasilnya bukan hanya API yang lebih aman, tetapi juga pengalaman developer yang lebih baik karena kegagalan pindah dari runtime ke pipeline, tempat masalah jauh lebih cepat dipahami dan diperbaiki.