Schema GraphQL adalah kontrak antara server dan semua client-nya. Toko Buku punya tiga: web toko, aplikasi kasir, dan dashboard admin, dan ketiganya gak bisa di-update bersamaan. Desain schema yang baik membuat schema bisa tumbuh tanpa memecah client yang sudah ada. Bayangin menu restoran yang sudah dicetak dan dibagikan ke pelanggan tetap. Mengganti nama menu membuat pesanan lama gak dikenali dapur. Lebih aman menambah menu baru, memberi tanda "segera diganti" pada yang lama, lalu menghapusnya saat gak ada lagi yang memesan.
🧪 Tebakan cepat: Tim mengganti nama field
pricejadipriceRupiah. Web toko yang belum di-update masih mengirim{ books { title price status } }. Apa jawabannya:pricejadinull, atau lebih buruk? Jawabannya ada di bagian "Deprecation, bukan versi baru".
Penamaan
| Bagian | Gaya | Contoh di Toko Buku |
|---|---|---|
| Type, input, enum, union | PascalCase, kata benda | Book, CoverUpload, StockStatus |
| Field dan argument | camelCase | coverUrl, priceRupiah, bookId |
| Nilai enum | SCREAMING_SNAKE_CASE | IN_STOCK, SOLD_OUT |
| Mutation | kata kerja dulu, lalu bendanya | createBook, updateStock, attachCover |
| Input dan payload | nama mutation + Input / Payload |
RestockBookInput, RestockBookPayload |
Mutation berawalan kata kerja langsung bilang apa yang terjadi. Nama type gak memakai kata kerja (Book, bukan GetBookResponse), karena type yang sama dipakai banyak operasi.
Input type dan payload type per mutation
updateStock(id: ID!, stock: Int!): Book gampang ditulis, tapi sulit dikembangkan. Bentuk yang dipakai banyak API besar:
type Mutation {
restockBook(input: RestockBookInput!): RestockBookPayload!
}
input RestockBookInput {
bookId: ID!
stock: Int!
}
type RestockBookPayload {
book: Book
userErrors: [UserError!]!
}
type UserError {
field: [String!]
message: String!
}
- Input type mengelompokkan argument yang saling terkait jadi satu objek. Client cukup mengirim satu variable
$input, dan input type bisa dipakai ulang, misalnyaAddressInputdi dalam beberapa input lain. Menambah field opsional ke input nanti gak memecah client lama. - Tiap mutation punya input sendiri, walaupun isinya mirip, supaya masing-masing bisa berubah tanpa saling mengganggu.
- Payload type membungkus hasilnya. Hari ini isinya
bookdanuserErrors; besok bisa ditambah field lain tanpa mengubah type kembalian mutation.userErrorsmembawa kesalahan input sebagai data, seperti unionStockErrordi pelajaran "Error Handling".
Nullability: di mana ! aman
Tanda ! adalah janji: nilainya selalu ada. Kalau janji itu gagal sekali saja, null-nya naik ke tempat terdekat yang boleh null (pelajaran "Schema & Type System").
- Aman non-null: nilai yang memang selalu ada dan dibaca dari tempat yang sama, seperti
id,title,price, dan list dari database sendiri ([Book!]!). - Sebaiknya nullable: field yang bisa ditolak aturan akses (
stockdi pelajaran "Authentication & Authorization"), dihitung dari layanan lain, atau bisa gagal sendiri. Di schema besar, kebanyakan field dibuat nullable supaya satu kegagalan cuma menghapus satu field, bukan seluruh response. - Arah perubahan menentukan aman atau gak. Untuk output: nullable jadi non-null aman, non-null jadi nullable berbahaya (client sudah menganggapnya selalu ada). Untuk argument dan input: wajib jadi opsional aman, opsional jadi wajib memecah client.
Deprecation, bukan versi baru
GraphQL gak butuh /v2. Field baru ditambahkan, field lama diberi @deprecated dengan alasan, lalu dihapus saat gak ada lagi yang memakainya:
type Book {
price: Int! @deprecated(reason: "Pakai priceRupiah.")
priceRupiah: Int!
}
Client lama tetap jalan: { books { title price status } } masih dijawab lengkap. Tool seperti GraphiQL dan linter memberi peringatan; aturan NoDeprecatedCustomRule di graphql-js menulisnya begini: The field Book.price is deprecated. Pakai priceRupiah.
Mengganti nama langsung berbeda jauh. Setelah price diganti priceRupiah, query web toko gak lolos validasi: Cannot query field "price" on type "Book". Seluruh operasinya ditolak, jadi judul dan status pun gak sampai. Itu jawaban tebakannya. Hal yang sama terjadi kalau updateStock(id, stock) langsung diubah jadi updateStock(input:): aplikasi kasir dapat Unknown argument "id" on field "Mutation.updateStock". Caranya: tambah restockBook(input:) di sampingnya, deprecate updateStock, lalu pantau pemakaian tiap field sampai nol sebelum menghapusnya.
Enum, bukan string
status: StockStatus! lebih baik dari status: String!. Enum mendaftar nilai yang mungkin di schema, dan nilai yang salah ditolak sebelum resolver jalan (Value "HABIS" does not exist in "StockStatus" enum.). Mengubah enum jadi String belakangan memecah client yang menulis books(status: SOLD_OUT): String cannot represent a non string value: SOLD_OUT. Menambah nilai enum baru juga perlu hati-hati, karena client yang memakai switch bisa bertemu nilai yang belum ia kenal.
Interface Node dan global ID
Relay, client GraphQL dari Meta, memakai pola global object identification: tiap objek bisa dicari ulang lewat satu field node.
interface Node {
id: ID!
}
type Book implements Node {
id: ID!
title: String!
}
type Query {
node(id: ID!): Node
}
- Interface adalah sekumpulan field yang wajib dimiliki tiap type yang
implementsdia. Bedanya dengan union (SearchResult = Book | Authordi pelajaran "Variables & Fragments"): union gak punya field bersama, interface punya. - Global ID unik di seluruh schema, bukan cuma di satu tabel. Biasanya nama type dan id lokal di-base64:
btoa("Book:1")jadi"Qm9vazox". Cache client bisa menyimpan objek apa pun hanya dengan id-nya. { node(id: "Qm9vazox") { ... on Book { title } } }menjawab Laskar Pelangi.
Mesin GraphQL mini di Lab belum menjalankan interface, jadi bagian ini cuma ditulis sebagai SDL.
Schema-first atau code-first
- Schema-first: SDL ditulis dulu (file
.graphql), lalu resolver mengikutinya. Seperti seluruh track ini. Schema-nya mudah dibaca dan dibahas tim, tapi kamu harus menjaga resolver tetap cocok dengan SDL. - Code-first: schema didefinisikan lewat kode, misalnya dengan Pothos, TypeGraphQL, atau Nexus, lalu SDL-nya di-generate. Keuntungan utamanya: schema dan implementasi selalu sinkron, dan type safety TypeScript lebih mudah; dengan Pothos atau TypeGraphQL, type-nya mengalir dari model sampai resolver tanpa langkah generate tambahan (Nexus punya langkah typegen sendiri).
⚠️ Jebakan yang sering ketemu
- Mengganti nama atau menghapus field langsung. Query client lama ditolak seluruhnya. Tambah yang baru,
@deprecatedyang lama. - Semua field non-null. Satu field yang gagal menghapus seluruh objek di atasnya, bahkan seluruh
data. - Banyak argument lepas di mutation. Menambah argument wajib memecah client. Mulai dengan satu
input. - Status berupa string bebas. Typo gak tertangkap, dan nilai yang mungkin gak terdokumentasi. Pakai enum.
TL;DR: Schema adalah kontrak dengan client yang gak di-update bersamaan. Type PascalCase, field camelCase, nilai enum SCREAMING_SNAKE_CASE, mutation diawali kata kerja. Tiap mutation memakai input type sendiri (mengelompokkan argument, bisa dipakai ulang, aman ditambah field) dan payload type (misalnya book plus userErrors). ! cuma untuk nilai yang pasti selalu ada; di schema besar kebanyakan field nullable. Jangan bikin versi baru: tambah field, beri @deprecated(reason:) pada yang lama, hapus setelah pemakaiannya nol; mengganti nama langsung membuat query lama ditolak (Cannot query field "price" on type "Book".). Pakai enum, bukan string. Interface Node dengan global ID (btoa("Book:1")) membuat tiap objek bisa dicari lewat node(id:). Union memilih salah satu dari beberapa object type, interface berbagi field. Schema-first menulis SDL dulu; code-first membuat SDL dari kode sehingga selalu sinkron dengan implementasi.