Variable adalah tempat kosong di query yang nilainya dikirim terpisah, sebagai JSON. Fragment adalah daftar field yang diberi nama supaya bisa dipakai di banyak tempat. Bayangin formulir pesanan di loket toko buku: teks formulirnya dicetak sama untuk semua pembeli ("Saya pesan buku nomor ___"), yang beda cuma isi kotak kosongnya. Itu variable. Sedangkan fragment seperti stempel "judul, harga, penulis" yang tinggal dicapkan di formulir mana pun.
🧪 Tebakan cepat: Query
BukuDetail($id: ID!)dikirim dengan variables{}, lupa mengisiid. Apa yang terjadi:bookjadinull, server memakai id 1, atau request ditolak sebelum resolver jalan? Jawabannya ada di bagian "Type variable, tanda seru, dan nilai default".
Masalahnya: nilai yang ditempel ke teks query
Cara pertama yang kepikiran biasanya menempel nilai ke teks query dengan template string:
const judul = 'Laskar "Pelangi"'; // dari form tambah buku
const query = `mutation {
createBook(input: { title: "${judul}", authorId: 1, price: 89000, stock: 1 }) {
id
}
}`;
Tanda kutip di judul itu menutup string GraphQL lebih awal, jadi server menolak seluruh query: Syntax Error: Expected ":", found String "". Lebih buruk lagi, isi form bisa sengaja disusun untuk menambah field atau argument lain ke query-mu. Ini masalah yang sama dengan SQL injection. Selain itu, tiap nilai menghasilkan teks query yang berbeda, jadi server gak bisa mengenali "query yang sama" di log atau cache-nya.
Variable: $id dan JSON di sebelahnya
Dengan variable, teks query-nya tetap. Nilainya dikirim di bagian lain dari body request:
query BukuDetail($id: ID!) {
book(id: $id) {
title
price
}
}
Variables:
{ "id": 2 }
{ "data": { "book": { "title": "Bumi Manusia", "price": 125000 } } }
BukuDetailadalah nama operation. Nama ini wajib kalau operation-nya punya variable.($id: ID!)adalah definisi variable: namanya diawali$, lalu type-nya.ID!artinya wajib diisi.book(id: $id)memakai variable itu sebagai nilai argument.- Di JSON, key-nya
"id", tanpa$.
Dari JavaScript, ketiganya dikirim dalam satu body:
const response = await fetch("https://api.tokobuku.test/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: BUKU_DETAIL, // teks query di atas, gak pernah berubah
operationName: "BukuDetail",
variables: { id: 2 },
}),
});
Ganti { "id": 2 } jadi { "id": 3 }, jawabannya Cantik Itu Luka, dan teks query-nya sama persis. Judul yang mengandung tanda kutip juga aman, karena nilai variable gak pernah dibaca sebagai teks GraphQL. Dia cuma nilai bertype String.
Type variable, tanda seru, dan nilai default
Server memeriksa variables terhadap type-nya sebelum resolver mana pun jalan. Kalau gak cocok, request ditolak utuh: response-nya cuma errors, tanpa data.
Yang dikirim untuk BukuDetail($id: ID!) |
Pesan dari graphql-js |
|---|---|
{} |
Variable "$id" has invalid value: Expected a value of non-null type "ID!" to be provided. |
{ "id": null } |
Variable "$id" has invalid value: Expected value of non-null type "ID!" not to be null. |
{ "id": true } |
Variable "$id" has invalid value: ID cannot represent value: true |
Jadi lupa mengisi id bukan bikin book jadi null, dan server gak menebak id 1. Request-nya ditolak sebelum resolver jalan. Itu jawaban tebakannya. Hal yang sama berlaku untuk UbahStok($id: ID!, $stock: Int!) dengan { "id": 2, "stock": "7" }: Variable "$stock" has invalid value: Int cannot represent non-integer value: "7". Angka di JSON ditulis tanpa kutip.
Variable boleh punya nilai default, dipakai kalau key-nya gak ada di JSON:
query DaftarBuku($status: StockStatus = IN_STOCK) {
books(status: $status) {
title
stock
}
}
{}memakai default: Laskar Pelangi dan Cantik Itu Luka.{ "status": "SOLD_OUT" }menimpa default: Bumi Manusia.{ "status": null }juga menimpa default.nullitu nilai, jadibooks(status: null)mengembalikan ketiga buku.{ "status": "HABIS" }ditolak:Variable "$status" has invalid value: Value "HABIS" does not exist in "StockStatus" enum.
Type variable harus input type (scalar, enum, atau input), dan harus cocok dengan tempat dia dipakai. $id: ID tanpa ! di book(id: ID!) ditolak waktu validasi: Variable "$id" of type "ID" used in position expecting type "ID!". Seluruh objek input juga bisa jadi satu variable: mutation TambahBuku($input: CreateBookInput!) dengan JSON { "input": { "title": "Ronggeng Dukuh Paruk", "authorId": 4, "price": 95000, "stock": 3 } }.
Fragment: daftar field yang dipakai ulang
Kartu buku di aplikasi selalu butuh judul, harga, dan nama penulis. Daripada menulis ketiganya berulang kali, beri nama sekali:
query DuaBuku {
laskar: book(id: 1) {
...InfoBuku
}
bumi: book(id: 2) {
...InfoBuku
stock
}
}
fragment InfoBuku on Book {
title
price
author {
name
}
}
{
"data": {
"laskar": { "title": "Laskar Pelangi", "price": 89000, "author": { "name": "Andrea Hirata" } },
"bumi": { "title": "Bumi Manusia", "price": 125000, "author": { "name": "Pramoedya Ananta Toer" }, "stock": 0 }
}
}
fragment InfoBuku on Bookmendefinisikan fragment bernamaInfoBukuuntuk typeBook....InfoBuku(tiga titik, disebut spread) menempelkan field-nya di situ. Field lain boleh ditambah di sebelahnya, sepertistock.- Response-nya gak punya key
InfoBuku. Field-nya langsung masuk ke objek buku. - Kalau kartu buku butuh field baru, cukup ubah fragment-nya, semua query ikut. Apollo Client dan Relay memakai pola ini: tiap komponen punya fragment untuk data yang dia tampilkan.
Aturannya ketat. Fragment yang gak dipakai bikin seluruh dokumen ditolak: Fragment "InfoBuku" is never used. Salah ketik nama: Unknown fragment "InfoBuk". Dipakai di type lain: Fragment "InfoBuku" cannot be spread here as objects of type "Author" can never be of type "Book".
Inline fragment: ... on Type
Untuk pelajaran ini, schema Toko Buku dapat field pencarian. Hasilnya bisa buku, bisa penulis, jadi type-nya union:
type Query {
# ...field yang sudah ada
search(text: String!): [SearchResult!]!
}
union SearchResult = Book | Author
Field dari tiap type diminta dengan inline fragment, fragment tanpa nama yang ditulis langsung di tempat:
query Cari($text: String!) {
search(text: $text) {
__typename
... on Book {
title
}
... on Author {
name
}
}
}
Dengan variables { "text": "ka" }:
{
"data": {
"search": [
{ "__typename": "Book", "title": "Laskar Pelangi" },
{ "__typename": "Book", "title": "Cantik Itu Luka" },
{ "__typename": "Author", "name": "Eka Kurniawan" }
]
}
}
... on Book { title } artinya "kalau hasil ini sebuah Book, ambil title-nya". __typename memberi tahu client type aslinya. Meminta title langsung di search ditolak: Cannot query field "title" on type "SearchResult". Did you mean to use an inline fragment on "Book"?
@include dan @skip
Dua directive bawaan ini membuang field berdasarkan variable Boolean:
query BukuDetail($id: ID!, $withAuthor: Boolean!) {
book(id: $id) {
title
author @include(if: $withAuthor) {
name
}
}
}
{ "id": 1, "withAuthor": false } menjawab { "book": { "title": "Laskar Pelangi" } }. Dengan true, author ikut. @skip(if: true) kebalikannya: field dibuang kalau kondisinya true. Keduanya juga bisa dipasang di ...InfoBuku dan ... on Book. Variable-nya harus Boolean! atau punya default; $withAuthor: Boolean ditolak dengan Variable "$withAuthor" of type "Boolean" used in position expecting type "Boolean!".
Beberapa operation dalam satu dokumen
Satu dokumen boleh berisi beberapa operation bernama, misalnya query Laskar { ... } dan query Bumi { ... }. Request-nya harus menyebut mana yang dijalankan lewat operationName: "Bumi". Tanpa itu: Must provide operation name if query contains multiple operations. Nama yang gak ada: Unknown operation named "Cantik". Nama operation juga yang muncul di log server dan devtools, jadi beri nama yang jelas.
⚠️ Jebakan yang sering ketemu
- Menempel nilai ke teks query. Template string seperti
"${judul}"rusak oleh tanda kutip dan membuka celah injection. Pakai variable. - Key JSON memakai
$.{ "$id": 2 }gak mengisi$id, jadi ditolak denganExpected a value of non-null type "ID!" to be provided.Tulis{ "id": 2 }. - Variable nullable di argument wajib.
$id: IDuntukbook(id: ID!)ditolak. Samakan type-nya:$id: ID!. - Fragment ditulis tapi gak dipakai. Satu fragment nganggur bikin seluruh dokumen ditolak.
- Field union diminta tanpa
... on. Di field ber-type union, cuma__typenameyang boleh diminta langsung.
TL;DR: Variable ($id: ID!) membuat teks query tetap, dan nilainya dikirim terpisah di JSON variables dengan key tanpa $, beserta operationName. Itu mencegah injection dan membuat query bisa dipakai ulang. Server memeriksa variables sebelum resolver jalan: yang wajib tapi kosong atau salah type bikin request ditolak tanpa data. Nilai default ($status: StockStatus = IN_STOCK) dipakai kalau key-nya gak ada, dan null menimpanya. Fragment (fragment InfoBuku on Book, dipakai dengan ...InfoBuku) adalah daftar field yang reusable untuk menghindari duplikasi; fragment yang gak dipakai ditolak. Inline fragment (... on Book) mengambil field per type di union seperti SearchResult. @include(if:) dan @skip(if:) membuang field berdasarkan Boolean. Dokumen dengan beberapa operation butuh operationName.