Mengembangkan skema melalui versioning field
Seiring berkembangnya kebutuhan aplikasi kita, GraphQL API yang menyuplai data ke dalamnya juga perlu berkembang, dengan memperkenalkan perubahan pada skemanya. Setiap kali perubahannya bersifat non-breaking, seperti menambahkan tipe atau field baru, kita dapat menerapkannya langsung tanpa takut efek samping. Namun ketika perubahannya bersifat breaking, kita perlu memastikan kita tidak memperkenalkan bug atau perilaku yang tidak terduga dalam aplikasi.
Perubahan breaking adalah perubahan yang menghapus tipe, field, atau direktif, atau memodifikasi tanda tangan dari field (atau direktif) yang sudah ada, seperti:
- Mengganti nama field
- Mengubah tipe argumen field yang sudah ada, atau menjadikannya wajib
- Menambahkan argumen wajib baru ke field
- Menambahkan non-nullable ke tipe respons sebuah field
Untuk menangani perubahan breaking, ada dua strategi utama: versioning dan evolusi, sebagaimana diimplementasikan oleh REST dan GraphQL, masing-masing.
REST API menunjukkan versi API yang digunakan baik pada URL endpoint (seperti https://api.mycompany.com/v1 atau https://api-v1.mycompany.com) atau melalui header tertentu (seperti Accept-version: v1). Melalui versioning, perubahan breaking ditambahkan ke versi baru API, dan karena klien harus secara eksplisit menunjuk ke versi baru API tersebut, mereka akan menyadari perubahan tersebut.
GraphQL tidak menolak penggunaan versioning, tetapi mendorong penggunaan evolusi. Sebagaimana dinyatakan di halaman GraphQL best practices:
While there's nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.
Evolusi berperilaku berbeda karena tidak diharapkan terjadi sekali setiap beberapa bulan, seperti halnya versioning. Melainkan, ini adalah proses berkelanjutan, yang terjadi bahkan setiap hari jika diperlukan, sehingga lebih cocok untuk iterasi cepat. Pendekatan ini telah dirumuskan oleh Principled GraphQL, serangkaian praktik terbaik untuk memandu pengembangan layanan GraphQL, dalam prinsip kelimanya:
5. Use an Agile Approach to Schema Development: The schema should be built incrementally based on actual requirements and evolve smoothly over time
Mengembangkan skema
Melalui evolusi, field dengan perubahan breaking harus melalui proses berikut:
- Mengimplementasikan ulang field tersebut dengan nama yang berbeda.
- Mendeprekasi field tersebut, meminta klien untuk menggunakan field baru sebagai gantinya.
- Setelah field tidak lagi digunakan oleh siapapun, hapus dari skema.
Mari kita lihat sebuah contoh. Katakanlah kita memiliki tipe Account, memodelkan akun sebagai orang dengan nama depan dan nama belakang melalui skema ini (menggunakan SDL GraphQL - Schema Definition Language):
type Account {
id: Int
name: String!
surname: String!
}Dalam skema ini, baik field name maupun surname adalah wajib (itulah simbol ! yang ditambahkan setelah tipe String) karena kita mengharapkan semua orang memiliki nama depan dan nama belakang.
Pada akhirnya, kita juga mengizinkan organisasi untuk membuka akun. Namun organisasi tidak memiliki nama belakang, sehingga kita harus mengubah tanda tangan field surname agar tidak wajib:
type Account {
id: Int
name: String!
surname: String # Ini telah berubah
}Ini adalah perubahan breaking karena aplikasi tidak mengharapkan field surname mengembalikan null, sehingga mungkin tidak memeriksa kondisi ini, seperti saat mengeksekusi kode JavaScript ini:
// Ini akan gagal ketika account.surname adalah null
const upperCaseSurname = account.surname.toUpperCase();Bug potensial yang dihasilkan dari perubahan breaking dapat dihindari dengan mengembangkan skema:
- Kita tidak memodifikasi tanda tangan field
surname; sebaliknya, kita menandainya sebagai deprecated, menambahkan pesan yang berguna yang menunjukkan nama field yang menggantikannya - Kita memperkenalkan nama field baru
personSurname(atauaccountSurname) ke skema
Tipe Account kita sekarang terlihat seperti ini:
type Account {
id: Int
name: String!
surname: String! @deprecated(reason: "Use `personSurname`")
personSurname: String
}Terakhir, dengan mengumpulkan log dari query klien kita, kita dapat menganalisis apakah mereka telah beralih ke field baru. Setiap kali kita melihat bahwa field surname tidak lagi digunakan oleh siapapun, kita kemudian dapat menghapusnya dari skema:
type Account {
id: Int
name: String!
personSurname: String
}Masalah dengan evolusi
Contoh yang dijelaskan di atas sangat sederhana, tetapi sudah menunjukkan beberapa masalah potensial dari mengembangkan skema:
| Masalah | Deskripsi |
|---|---|
| Nama field menjadi kurang rapi | Pertama kali kita memberi nama field, kita mungkin akan menemukan nama yang optimal untuknya, seperti surname. Ketika kita perlu menggantinya, kita harus membuat nama yang berbeda yang mungkin tidak optimal (yang optimal sudah digunakan!). Semua kemungkinan pengganti dalam contoh di atas memiliki masalah:- personName membuat eksplisit bahwa akun tersebut untuk seseorang, sehingga jika nantinya kita harus membuka akun untuk entitas non-manusia dengan nama belakang (entah apa... orang Mars?), maka kita perlu mengembangkan skema lagi agar nama-namanya tetap konsisten- Bagian "account" dalam accountName sepenuhnya redundan karena tipenya sudah Account- Selain itu, nama lain apa yang harus digunakan? surname1? surnameNew? Atau lebih buruk lagi, surnameV2?Akibatnya, skema yang diperbarui akan kurang mudah dipahami dan lebih verbose. |
| Skema dapat mengumpulkan field yang deprecated | Mendeprekasi field paling masuk akal sebagai keadaan sementara; pada akhirnya, kita benar-benar ingin menghapus field-field tersebut dari skema untuk membersihkannya sebelum mulai menumpuk. Namun, mungkin ada klien yang tidak merevisi query mereka dan masih mengambil informasi dari field yang deprecated. Dalam hal ini, skema kita akan perlahan tapi pasti menjadi semacam pemakaman field, mengumpulkan beberapa field berbeda untuk fungsionalitas yang sama. |
Mari kita lihat bagaimana menyelesaikan masalah-masalah ini.
Versioning field
Kita dapat membuat field kita dengan argumen bernama version, yang melaluinya kita menentukan versi field mana yang akan digunakan.
Dalam skenario ini, kita tetap harus menyimpan implementasi untuk field yang deprecated, sehingga kita tidak meningkat dalam hal tersebut. Namun, kontraknya menjadi tersembunyi: field baru sekarang dapat mempertahankan nama aslinya (tidak perlu mengganti nama dari surname menjadi personSurname), mencegah skema kita menjadi terlalu verbose.
Perlu diperhatikan bahwa konsep versioning ini berbeda dari REST:
- REST menetapkan situasi semua-atau-tidak-sama-sekali di mana seluruh API yang di-query memiliki versi yang sama karena versi yang digunakan adalah bagian dari endpoint
- Dalam pendekatan lain ini, setiap field di-version secara independen
Oleh karena itu, kita dapat mengakses versi yang berbeda untuk field yang berbeda, seperti ini:
query GetPosts {
posts(version: "1.0.0") {
id
title(version: "2.1.1")
url
author {
id
name(version: "1.5.3")
}
}
}Selain itu, dengan mengandalkan semantic versioning, kita dapat menggunakan version constraints untuk memilih versi, mengikuti aturan yang sama yang digunakan oleh Composer untuk mendeklarasikan dependensi paket. Kemudian, kita mengganti nama argumen field version menjadi versionConstraint dan memperbarui query:
query GetPosts {
posts(versionConstraint: "^1.0") {
id
title(versionConstraint: ">=2.1")
url
author {
id
name(versionConstraint: "~1.5.3")
}
}
}Menerapkan strategi ini pada field surname yang deprecated, kita sekarang dapat menandai implementasi yang deprecated sebagai versi "1.0.0" dan implementasi baru sebagai versi "2.0.0" dan mengakses keduanya, bahkan dalam query yang sama:
query GetSurname {
account(id: 1) {
oldVersion: surname(versionConstraint: "^1.0")
newVersion: surname(versionConstraint: "^2.0")
}
}Fitur ini tersedia di Gato GraphQL:

Versioning direktif
Karena direktif juga menerima argumen, kita dapat mengimplementasikan metodologi yang persis sama untuk melakukan versioning pada direktif juga!
Misalnya, saat menjalankan query ini:
query {
post(by: { id: 1 }) {
oldVersion: title @strTitleCase(versionConstraint: "^0.1")
newVersion: title @strTitleCase(versionConstraint: "^0.2")
}
}Ini dapat menghasilkan respons yang berbeda untuk setiap versi direktif:
