GraphQL di Production — GraphQL

GraphQL di Production Checklist dan best practices untuk menjalankan GraphQL di production. Security Checklist Query depth limiting — Prevent deep nesting…

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 } }, dengan stock salah 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:

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

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

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.

Yang akan kamu pelajari