Schema adalah daftar lengkap apa saja yang bisa diminta dari sebuah API GraphQL: type apa yang ada, field apa di tiap type, dan nilai seperti apa yang boleh ada di tiap field. Server menolak semua query yang gak cocok dengan schema, sebelum satu pun resolver jalan. Bayangin formulir resmi di kantor: tiap kolom punya jenis isian (angka, teks, tanggal), kolom bertanda bintang wajib diisi, dan beberapa kolom cuma boleh dicentang dari pilihan yang tersedia. Schema GraphQL persis seperti itu, dan tanda bintangnya adalah !.
🧪 Tebakan cepat: Resolver penulis gagal untuk satu dari tiga buku, jadi
author-nyanull. Schema-nyabooks: [Book!]!danauthor: Author!. Berapa buku yang sampai ke client? Jawabannya ada di bagian "List dan tanda seru".
Schema Toko Buku
Schema ditulis dalam SDL (Schema Definition Language). Ini schema Toko Buku yang dipakai di seluruh jalur GraphQL:
type Query {
book(id: ID!): Book
books(status: StockStatus): [Book!]!
author(id: ID!): Author
authors: [Author!]!
}
type Mutation {
createBook(input: CreateBookInput!): Book!
updateStock(id: ID!, stock: Int!): Book
}
enum StockStatus {
IN_STOCK
SOLD_OUT
}
type Book {
id: ID!
title: String!
price: Int!
stock: Int!
status: StockStatus!
author: Author!
}
type Author {
id: ID!
name: String!
books: [Book!]!
}
input CreateBookInput {
title: String!
authorId: ID!
price: Int!
stock: Int!
}
type Queryadalah pintu masuk untuk membaca data. Tiap field-nya bisa ditulis paling luar di sebuah query, misalnyabook(id: 1).type Mutationadalah pintu masuk untuk mengubah data. Dibahas di pelajaran "Mutations".type Bookdantype Authoradalah object type: kumpulan field.author: Author!menghubungkan buku ke penulisnya, danbooks: [Book!]!sebaliknya.enumdaninputdibahas di bawah.
Scalar bawaan
Field paling ujung, yang gak punya field lagi di dalamnya, bernilai scalar (atau enum). GraphQL punya tepat lima scalar bawaan:
| Scalar | Isinya | Contoh di Toko Buku |
|---|---|---|
Int |
bilangan bulat 32-bit, dari -2.147.483.648 sampai 2.147.483.647 | price: 89000 |
Float |
angka desimal | (belum dipakai) |
String |
teks | title: "Laskar Pelangi" |
Boolean |
true atau false |
(belum dipakai) |
ID |
penanda unik, dikirim sebagai teks | id: "2" |
ID selalu dikirim sebagai string di response. Data Toko Buku menyimpan id sebagai angka 2, tapi book(id: 2) { id } menjawab "id": "2". Sebagai argument, id: 2 dan id: "2" sama-sama diterima.
Date bukan scalar bawaan. Schema yang memakai publishedAt: Date tanpa deklarasi ditolak dengan pesan Unknown type "Date".. Kalau butuh, deklarasikan custom scalar dengan scalar Date, lalu server yang menentukan cara membaca dan mengirimnya (biasanya string seperti "2026-10-11").
Tanda seru: non-null
! artinya non-null: nilainya gak boleh null.
- Di field hasil,
title: String!adalah janji server: tiap buku pasti punya judul. Client gak perlu mengeceknull. - Di argument,
book(id: ID!)artinya client wajib mengirimid. Tanpa itu, query ditolak:Argument "Query.book(id:)" of type "ID!" is required, but it was not provided. - Tanpa
!, field bolehnull.book(id: ID!): Bookbisa menjawabnulluntuk buku yang gak ada.
Kalau resolver mengembalikan null untuk field ber-!, server gak mengirim null itu. Dia mencatat error Cannot return null for non-nullable field Book.author., lalu menaikkan null-nya ke tempat terdekat yang boleh null.
List dan tanda seru
[Book] artinya list berisi buku. Tanda seru bisa ada di dua tempat: di dalam kurung siku untuk tiap isinya, di luar untuk list-nya sendiri. Ini yang terjadi pada query { books { title author { name } } } kalau penulis Cantik Itu Luka (buku ketiga) jadi null:
Schema books |
Artinya | data yang sampai ke client |
|---|---|---|
[Book!]! |
list gak pernah null, isinya juga gak | null |
[Book]! |
list gak pernah null, isinya boleh null | { "books": [buku 1, buku 2, null] } |
[Book!] |
list boleh null, isinya gak | { "books": null } |
[Book] |
dua-duanya boleh null | { "books": [buku 1, buku 2, null] } |
Keempatnya juga membawa error Cannot return null for non-nullable field Book.author. dengan "path": ["books", 2, "author"]. null dari author naik ke bukunya (Book! gak boleh null), lalu ke list-nya, sampai ketemu tempat yang boleh null. Di schema Toko Buku yang asli ([Book!]!), gak ada tempat seperti itu, jadi data seluruhnya jadi null. Itu jawaban tebakannya: nol buku sampai ke client.
Kalau author dibuat boleh null (author: Author), hasilnya tiga buku, buku ketiga dengan "author": null, tanpa error sama sekali. Jadi pakai ! untuk yang memang pasti ada, dan biarkan boleh null untuk yang datang dari layanan yang bisa gagal.
Enum: pilihan yang terbatas
Pakai enum kalau sebuah field cuma boleh berisi satu dari beberapa pilihan tetap. StockStatus cuma IN_STOCK atau SOLD_OUT, dan resolver status mengisinya dari stok (stok 0 berarti SOLD_OUT):
{
books(status: SOLD_OUT) {
title
stock
status
}
}
{ "data": { "books": [{ "title": "Bumi Manusia", "stock": 0, "status": "SOLD_OUT" }] } }
Nilai enum ditulis tanpa tanda kutip. Mesin GraphQL memeriksanya sebelum resolver jalan:
books(status: HABIS)ditolak:Value "HABIS" does not exist in "StockStatus" enum.books(status: "SOLD_OUT")juga ditolak, karena itu string:Enum "StockStatus" cannot represent non-enum value: "SOLD_OUT". Did you mean the enum value "SOLD_OUT"?
Kalau status bertype String, "HABIS" lolos validasi dan resolver yang harus mengeceknya sendiri. Enum memberi pengecekan itu gratis dari mesin GraphQL.
Input type: argument berbentuk objek
Argument yang isinya banyak field dikumpulkan dalam input type. createBook(input: CreateBookInput!) menerima satu objek berisi title, authorId, price, dan stock. Input type ditulis dengan input, bukan type, dan cuma boleh berisi scalar, enum, list, atau input type lain. Book gak bisa dipakai sebagai argument, dan CreateBookInput gak bisa jadi hasil field. Contoh pemakaiannya ada di pelajaran "Mutations".
⚠️ Jebakan yang sering ketemu
!di semua field. Satunulldi bagian dalam bisa menghapus seluruhdata, seperti di tebakan tadi. Pasang!hanya di field yang memang selalu ada.- String untuk nilai yang pilihannya tetap. Typo seperti
"HABIS"lolos. Pakai enum. - Mengira
IDitu angka. Response-nya"2", string. Bandingkan denganString(book.id) === args.iddi resolver. Intuntuk angka yang terlalu besar.3000000000ditolak:Int cannot represent non 32-bit signed integer value: 3000000000. Harga dalam rupiah masih aman sampai 2.147.483.647.
📌 Interface dan union
Interface adalah daftar field yang wajib dimiliki semua type yang mengimplementasinya. Union cuma kumpulan type, tanpa field bersama yang wajib.
interface Product {
id: ID!
title: String!
price: Int!
}
type Ebook implements Product {
id: ID!
title: String!
price: Int!
fileSizeMb: Float!
}
union SearchResult = Book | Author
(Book ditulis type Book implements Product { ... } dengan field-nya yang tadi.) Di field ber-type Product, title dan price boleh diminta langsung karena pasti ada. Di SearchResult, field-nya harus diminta per type dengan inline fragment ... on Type, dan __typename memberi tahu type aslinya:
{
search(text: "ka") {
__typename
... on Book {
title
}
... on Author {
name
}
}
}
{
"data": {
"search": [
{ "__typename": "Book", "title": "Laskar Pelangi" },
{ "__typename": "Book", "title": "Cantik Itu Luka" },
{ "__typename": "Author", "name": "Eka Kurniawan" }
]
}
}
Meminta title langsung di search ditolak: Cannot query field "title" on type "SearchResult". Did you mean to use an inline fragment on "Product" or "Book"?
📌 Directive: @skip, @include, @deprecated
Directive adalah penanda berawalan @ yang mengubah cara sesuatu diproses.
@skip(if: true)melewati field kalau kondisinyatrue:{ book(id: 1) { title price @skip(if: true) } }menjawab{ "book": { "title": "Laskar Pelangi" } }. Kebalikannya,@include(if: false)juga membuang field itu. Biasanya kondisinya berupa variable, dibahas di pelajaran "Variables & Fragments".@deprecated(reason: "...")dipasang di schema untuk menandai field yang sudah usang:stockCount: Int! @deprecated(reason: "Pakai stock."). Field itu masih bisa diminta dan masih menjawab, tapi tool seperti GraphiQL menampilkan peringatan beserta alasannya.
📌 Introspection: schema bisa ditanya
Schema bisa ditanyakan lewat query juga. Field khusus berawalan dua garis bawah, seperti __type, disebut introspection:
{
__type(name: "Book") {
name
kind
fields {
name
}
}
}
Jawabannya "name": "Book", "kind": "OBJECT", dan field id, title, price, stock, status, author. Dari sini GraphiQL dan editor membuat autocomplete. Beberapa server, misalnya Apollo Server, mematikan introspection secara default di production. Mesin di Lab belum mendukung introspection.
TL;DR: Schema, ditulis dalam SDL, adalah kontrak semua type dan field yang bisa diminta; query yang gak cocok ditolak sebelum resolver jalan. Scalar bawaan cuma lima: Int (32-bit), Float, String, Boolean, ID (dikirim sebagai string); Date bukan bawaan, jadi deklarasikan scalar Date. ! artinya non-null: di hasil itu janji server, di argument itu wajib diisi. null di field ber-! naik ke tempat terdekat yang boleh null, jadi dengan [Book!]! dan author: Author! satu penulis yang gagal bikin data seluruhnya null. Enum untuk pilihan terbatas, divalidasi otomatis oleh mesin GraphQL (string gak). Input type untuk argument berbentuk objek. Interface mewajibkan field bersama, union gak punya field bersama dan diakses dengan ... on Type. @skip melewati field kalau kondisinya true, @deprecated menandai field usang beserta alasannya.