Mutation adalah operasi GraphQL untuk mengubah data di server: membuat, mengubah, atau menghapus. Perannya mirip POST, PUT, PATCH, dan DELETE di REST, sedangkan query mirip GET. Bayangin kasir di toko buku. Kamu menyerahkan formulir perubahan ("tambah buku ini", "ubah stok buku itu"), kasir memprosesnya satu per satu sesuai urutan, lalu memberi struk. Isi struknya kamu sendiri yang pilih, sama seperti query.
🧪 Tebakan cepat: Satu request berisi dua mutation:
first: updateStock(id: 3, stock: 4)lalusecond: updateStock(id: 3, stock: 2). Stok akhir Cantik Itu Luka berapa, 4 atau 2? Jawabannya ada di bagian "Dua mutation dalam satu request".
Mutation di schema
Mutation adalah field di type Mutation. Di schema Toko Buku:
type Mutation {
createBook(input: CreateBookInput!): Book!
updateStock(id: ID!, stock: Int!): Book
}
input CreateBookInput {
title: String!
authorId: ID!
price: Int!
stock: Int!
}
createBookmenerima satu argumentinputberisi data buku baru, dan mengembalikanBook!: buku yang baru dibuat, pasti ada.updateStockmenerimaiddanstockbaru, dan mengembalikanBookyang bolehnull.- Resolver-nya yang benar-benar mengubah data. Schema cuma menjanjikan bentuk argument dan hasilnya.
Membuat buku: createBook
Ahmad Tohari belum punya buku di toko. Tambahkan Ronggeng Dukuh Paruk:
mutation {
createBook(input: {
title: "Ronggeng Dukuh Paruk"
authorId: 4
price: 95000
stock: 3
}) {
id
title
author {
name
}
}
}
{
"data": {
"createBook": {
"id": "4",
"title": "Ronggeng Dukuh Paruk",
"author": { "name": "Ahmad Tohari" }
}
}
}
- Kata
mutationdi depan wajib. Tanpa itu, operasinya dianggap query. input: { ... }adalah objekCreateBookInput. Field-nya dipisah baris baru (koma boleh, gak wajib).- Setelah kurung argument ada selection set, persis seperti query. Kamu memilih apa yang dikembalikan dari buku yang baru dibuat: di sini
id,title, dan nama penulisnya. Field lain gak dikirim. iddibuat server: buku keempat dapat"4". Toko sekarang punya 4 buku.
Mengubah stok: updateStock
Bumi Manusia habis (stok 0). Datang kiriman 7 buku:
mutation {
updateStock(id: 2, stock: 7) {
title
stock
status
}
}
{ "data": { "updateStock": { "title": "Bumi Manusia", "stock": 7, "status": "IN_STOCK" } } }
status ikut berubah dari SOLD_OUT jadi IN_STOCK, karena resolver-nya menghitung status dari stok. Itu gunanya mutation mengembalikan object yang berubah: client langsung tahu keadaan barunya tanpa query kedua.
Dua mutation dalam satu request
Satu operation mutation boleh berisi beberapa field. Pakai alias supaya key-nya beda:
mutation {
first: updateStock(id: 3, stock: 4) {
stock
}
second: updateStock(id: 3, stock: 2) {
stock
}
}
{ "data": { "first": { "stock": 4 }, "second": { "stock": 2 } } }
Spesifikasi GraphQL mewajibkan field teratas sebuah mutation dijalankan berurutan (serial): first selesai seluruhnya, termasuk selection set-nya, baru second mulai. Jadi stok akhirnya 2. Itu jawaban tebakannya. Field teratas sebuah query boleh dijalankan bersamaan, karena membaca data gak saling mengganggu.
Kalau mutation gagal
Ada dua jenis kegagalan. Validasi menolak operation sebelum apa pun dijalankan, jadi gak ada data yang berubah dan response-nya cuma errors. Resolver bisa menolak waktu dijalankan, dan hasil field-nya jadi null.
| Yang dikirim | Ditolak oleh | Response |
|---|---|---|
updateStock(id: 2, stock: -1) |
resolver | "Stock cannot be negative", data: { "updateStock": null } |
createBook dengan authorId: 9 |
resolver | "Author not found", data: null |
createBook tanpa stock di input |
validasi | Expected value of type "CreateBookInput" to include required field "stock", found: { title: "Ronggeng Dukuh Paruk", authorId: 4, price: 95000 }. |
updateStock(id: 2, stock: "7") |
validasi | Int cannot represent non-integer value: "7" |
updateStock(id: 2, stock: 7) tanpa selection set |
validasi | Field "updateStock" of type "Book" must have a selection of subfields. Did you mean "updateStock { ... }"? |
createBook di dalam query { ... } |
validasi | Cannot query field "createBook" on type "Query". |
Lihat bedanya dua baris pertama. updateStock mengembalikan Book yang boleh null, jadi cuma field itu yang null. createBook mengembalikan Book!, jadi null-nya naik sampai data seluruhnya null. Pesan "Stock cannot be negative" dan "Author not found" ditulis resolver Toko Buku. Pesan validasi datang dari mesin GraphQL (di sini graphql-js 17.0.2).
Penamaan yang dipakai banyak API
- Nama mutation: kata kerja + benda, camelCase:
createBook,updateStock,deleteBook. - Data yang banyak field-nya dikumpulkan di satu argument
input, dengan input type bernama sesuai mutation-nya:createBook(input: CreateBookInput!). - Kembalikan object yang berubah (
Book), bukan cumatrue. Client bisa memilih field yang dia perlukan dan memperbarui tampilannya. - Beri nama operation-nya supaya gampang dicari di log:
mutation TambahBuku { createBook(...) { id } }.
⚠️ Jebakan yang sering ketemu
- Lupa kata
mutation.{ createBook(...) { id } }dianggap query, dan ditolak denganCannot query field "createBook" on type "Query". - Mengirim mutation lewat GET. Server menolaknya dengan status 405. Mutation selalu lewat POST.
- Mengira beberapa mutation jalan bersamaan. Field teratas mutation selalu dijalankan berurutan, dari atas ke bawah.
- Cuma mengecek status HTTP. Stok negatif di banyak server tetap dijawab 200, dengan
errorsdi body.
📌 Payload dengan pesan untuk pengguna
Banyak API besar tidak melempar error untuk kesalahan yang wajar dari pengguna, seperti stok negatif. Mutation-nya mengembalikan payload: type khusus yang berisi hasilnya dan daftar masalahnya, misalnya type UpdateStockPayload { book: Book, userErrors: [UserError!]! }. Client cukup membaca userErrors untuk menampilkan pesan di form, dan errors di response disisakan untuk kesalahan yang memang tak terduga. Shopify, misalnya, memakai pola userErrors ini di API GraphQL-nya. Pola ini dibahas lagi di pelajaran "Error Handling di GraphQL".
TL;DR: Mutation mengubah data di server (create, update, delete), seperti POST, PUT, dan DELETE di REST, dan wajib ditulis dengan kata mutation. Field-nya ada di type Mutation, menerima argument (sering satu input bertype input), dan mengembalikan object yang berubah. Kamu memilih field hasilnya sendiri: createBook(...) { id title } cuma mengembalikan id dan title buku baru. Beberapa field teratas di satu mutation dijalankan berurutan, jadi stok 4 lalu 2 berakhir di 2. Validasi menolak sebelum apa pun berubah; resolver yang menolak bikin field-nya null (dan data: null kalau hasilnya Book!). Namai dengan kata kerja + benda (createBook, updateStock), dan jangan pernah kirim mutation lewat GET.