Membawa GraphQL ke production artinya server-mu melayani pengguna sungguhan, orang asing yang iseng, dan kerusakan yang datang tanpa permisi, sekaligus. Bayangin hari pertama Toko Buku dibuka untuk umum. Ada CCTV dan buku catatan penjualan supaya kamu tahu apa yang terjadi, batas belanja per orang supaya satu pembeli gak menghabiskan gudang, pintu gudang yang dikunci, satpam yang cuma menerima pesanan lewat kasir, dan daftar periksa yang dicentang sebelum pintu dibuka.
🧪 Tebakan cepat: Introspection sudah dimatikan. Penyerang mengirim
query Intip { book(id: 1) { stok } }, denganstocksalah ketik. Bisakah ia tahu nama field yang benar? Jawabannya ada di bagian "Introspection mati bukan pengaman".
Observability: tahu apa yang terjadi
Observability adalah kemampuan melihat ke dalam server yang sedang jalan: request apa yang masuk, berapa lama, dan apa yang gagal. Di GraphQL ada tiga hal yang wajib:
- Nama operation wajib. Tanpa nama, log server cuma berisi
POST /graphql 200untuk semua request. Dengan nama, kamu bisa melihatKataloglambat danRestocksering gagal. Tolak operation tanpa nama dengan plugin. - Waktu per resolver (tracing). Satu request menjalankan banyak resolver, jadi waktu total saja gak cukup.
- Pemakaian field per client. Field mana dipakai aplikasi mana. Ini yang memberi tahu kapan field
@deprecatedsudah aman dihapus. Apollo Client mengirim nama aplikasinya lewat headerapollographql-client-name.
const observability = {
async requestDidStart({ request }) {
const client = request.http?.headers.get("apollographql-client-name") ?? "unknown";
return {
async didResolveOperation({ operation }) {
if (!operation.name) {
throw new GraphQLError("Operation name required", {
extensions: { code: "OPERATION_NAME_REQUIRED", http: { status: 400 } },
});
}
},
async executionDidStart() {
return {
willResolveField({ info }) {
const start = performance.now();
return () => metrics.record(`${info.parentType.name}.${info.fieldName}`, performance.now() - start, client);
},
};
},
};
},
};
willResolveField dipanggil untuk tiap field; fungsi yang dikembalikannya dipanggil saat field itu selesai. metrics.record adalah sistem metrik-mu sendiri. Di Lab, database pura-puranya butuh 12 ms untuk daftar buku dan 8 ms untuk satu penulis. Untuk { books { title author { name } } }, tracing-nya:
| Resolver | Mulai | Selesai |
|---|---|---|
Query.books |
0 ms | 12 ms |
Book.author (3 kali, bersamaan) |
12 ms | 20 ms |
Total 20 ms, dan langsung kelihatan ada 3 query penulis (pelajaran N+1). Alat siap pakai: usage reporting Apollo GraphOS, atau OpenTelemetry dengan @opentelemetry/instrumentation-graphql.
Error masking
Dengan NODE_ENV=production, Apollo Server 5 membuang stacktrace dari response, tapi pesannya tetap terkirim apa adanya. Database yang mati tetap muncul ke pengguna:
{"errors":[{"message":"connect ECONNREFUSED 10.0.0.5:5432","locations":[{"line":1,"column":20}],"path":["updateStock"],"extensions":{"code":"INTERNAL_SERVER_ERROR"}}],"data":{"updateStock":null}}
Samarkan dengan formatError dari pelajaran "Error Handling": INTERNAL_SERVER_ERROR jadi "Internal server error", pesan aslinya dicatat di log.
Batas biaya, rate limit, dan timeout
- Per request:
depthLimit(5)dan biaya maksimum 200 dari pelajaran "Performance & Caching".Dalam(depth 6) danBorong(biaya 800) ditolak sebelum resolver mana pun jalan. - Rate limit berdasarkan biaya: tiap token dapat anggaran poin per waktu, misalnya 1.000 poin per menit, dan tiap request mengurangi anggaran sebesar biayanya. Kalau habis, jawab
429 Too Many Requestsdengan headerRetry-After. Ini yang menahan seribu query kecil yang masing-masing lolos batas per request. GitHub memakai cara ini untuk API GraphQL-nya: 5.000 poin per jam per pengguna,firstdanlastharus 1 sampai 100, dan maksimal 500.000 node per panggilan. - Timeout: hentikan request yang terlalu lama di router atau HTTP server (GitHub menghentikan request yang lebih dari 10 detik), dan pasang batas di database juga, misalnya
statement_timeoutdi PostgreSQL.
Introspection mati bukan pengaman
Introspection (query __schema dan __type) mengembalikan seluruh schema. Kalau dibiarkan aktif di production, penyerang bisa memetakan semua type, field, dan relasinya, termasuk yang sensitif. Apollo Server 5 mematikannya secara bawaan saat NODE_ENV=production, dan menjawab dengan status 400:
GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production
Tapi itu belum menutup semuanya. Validasi graphql-js tetap memberi saran untuk field yang salah ketik:
Cannot query field "stok" on type "Book". Did you mean "stock"?
Jadi ya, penyerang tahu nama field yang benar walau introspection mati. Itu jawaban tebakannya. Dengan menebak berulang-ulang, alat seperti Clairvoyance bisa menyusun ulang sebagian besar schema. Opsi hideSchemaDetailsFromClientErrors: true di Apollo Server menghapus bagian Did you mean ...?. Lagi pula, operation milik client-mu ada di bundle JavaScript yang bisa dibaca siapa saja. Jadi mematikan introspection itu lapisan tambahan, bukan pengganti authorization di tiap field (pelajaran "Authentication & Authorization") dan trusted documents.
CSRF
Dari pelajaran "File Upload": request dengan content type "simple" (text/plain, multipart/form-data, application/x-www-form-urlencoded) atau tanpa content type bisa dikirim browser dari situs lain tanpa bertanya. Apollo Server menolaknya dengan status 400 (This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). ...) kecuali ada header Apollo-Require-Preflight atau X-Apollo-Operation-Name yang gak kosong. csrfPrevention aktif secara bawaan sejak Apollo Server 4; jangan dimatikan. Apollo Client selalu mengirim content-type: application/json, bahkan untuk GET, jadi request-nya tetap lolos.
Schema registry dan versi
Schema disimpan di git, dan tiap pull request dicek di CI (graphql-inspector diff dan test kontrak dari pelajaran "Testing GraphQL APIs"). Schema registry seperti Apollo GraphOS atau GraphQL Hive menyimpan tiap versi schema yang dipublikasikan beserta operation yang benar-benar dipakai client, jadi pemeriksaan perubahan bisa memakai lalu lintas sungguhan dan changelog-nya terbentuk sendiri. Versinya tetap satu: tambah field, beri @deprecated, pantau pemakaiannya, hapus setelah nol. Gak ada /v2.
Checklist go-live Toko Buku
| Item | Di Toko Buku |
|---|---|
| Nama operation wajib | plugin menolak operation tanpa nama (400 OPERATION_NAME_REQUIRED) |
| Tracing dan pemakaian field | waktu per resolver dan field per client (apollographql-client-name) |
| Batas depth dan biaya | depthLimit(5), biaya maksimum 200 |
| Rate limit per token | misalnya 1.000 poin per menit, 429 kalau habis |
| Timeout | di router atau HTTP server, dan statement_timeout di database |
| Error masking | formatError menyamarkan INTERNAL_SERVER_ERROR, aslinya ke log |
| Introspection dan saran | introspection: false, hideSchemaDetailsFromClientErrors: true |
| CSRF | csrfPrevention tetap aktif |
| Trusted documents | web toko, kasir, dan admin cuma mengirim hash yang terdaftar |
| DataLoader per request | loader baru di context |
| Cek schema di CI | graphql-inspector diff dan test kontrak, registry untuk riwayatnya |
⚠️ Jebakan yang sering ketemu
- Introspection mati dianggap cukup. Saran
Did you meandan bundle client tetap membocorkan nama field. Authorization yang menjaga data. NODE_ENV=productiondianggap menyamarkan error. Yang hilang cumastacktrace; pesan internal tetap terkirim tanpaformatError.- Batas biaya tanpa rate limit. Banyak query kecil yang masing-masing lolos tetap bisa membanjiri server.
- Operation tanpa nama. Log dan statistik gak bisa membedakan satu query dari yang lain.
TL;DR: Production butuh observability: nama operation wajib, waktu per resolver dari tracing, dan pemakaian field per client sebelum menghapus field. NODE_ENV=production cuma membuang stacktrace, jadi samarkan pesan internal dengan formatError. Tolak query berat dengan depthLimit(5) dan batas biaya 200, batasi tiap token dengan rate limit berdasarkan biaya (429), dan pasang timeout di server dan database. Introspection membuka seluruh schema, jadi matikan di production, tapi itu bukan pengaman: Did you mean "stock"? tetap membocorkan nama field sampai hideSchemaDetailsFromClientErrors dinyalakan, dan authorization per field yang menjaga data. Biarkan csrfPrevention aktif. Simpan schema di git dan registry, cek tiap perubahan di CI, dan pakai @deprecated daripada versi baru. Checklist go-live Toko Buku merangkum semuanya.