Variables & Fragments — GraphQL

Variables & Fragments Variables Jangan concatenate values ke query string — gunakan variables. Sama seperti parameterized queries di SQL (prevent injection…

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 mengisi id. Apa yang terjadi: book jadi null, 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 } } }

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
  }
}

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 }
  }
}

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

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.

Yang akan kamu pelajari