API publik cenderung berubah: field baru ditambah, field lama dihapus, struktur respons direstrukturisasi. Tanpa versioning, perubahan breaking akan merusak klien yang sudah terlanjur produksi. Versioning memberi jalur migrasi — klien lama tetap memakai versi lama, klien baru pindah ke versi baru. Pada chapter ini kita akan membandingkan tiga strategi versioning: URL, header, dan content negotiation.
URL Versioning #
Strategi paling populer adalah menanam versi di URL: /v1/users, /v2/users, /v3/users. Klien memilih versi dengan mengubah path; server memilih handler berdasarkan prefix.
GET /v1/users -> {"id":1,"nama":"Budi Santoso"}
GET /v2/users -> {"id":1,"namaDepan":"Budi","namaBelakang":"Santoso"}
GET /v3/users -> {"id":1,"namaDepan":"Budi","namaBelakang":"Santoso","email":"[email protected]"}Kelebihan: jelas terlihat di log, mudah di-debug, kompatibel dengan cache HTTP (URL berbeda = cache berbeda). Kekurangan: URL menjadi panjang dan banyak versi dapat aktif bersamaan, meningkatkan beban maintenance.
Header Versioning #
Header versioning menyimpan versi di header HTTP X-API-Version. Klien mengirim header ini; server membaca dan memilih handler.
GET /users
X-API-Version: 2
Response:
{"id":1,"namaDepan":"Budi","namaBelakang":"Santoso"}Kelebihan: URL tetap bersih. Kekurangan: tidak terlihat di log URL, sulit diuji via browser tanpa ekstensi. Beberapa proxy/cache mungkin tidak menyertakan header custom.
Content Negotiation #
Content negotiation memakai header Accept dengan custom media type: application/vnd.example.v3+json. Versi dibedakan via media type, bukan URL atau header custom.
GET /users
Accept: application/vnd.example.v3+json
Response:
Content-Type: application/vnd.example.v3+json
{"id":1,"namaDepan":"Budi","namaBelakang":"Santoso","email":"[email protected]"}Kelebihan: mengikuti semantik HTTP asli. Kekurangan: media type custom jarang dipahami tool umum (mis. Postman default tetap application/json).
Deprecation dan Sunset #
Saat versi lama dihapus, klien perlu diperingatkan. HTTP RFC mendefinisikan header Deprecation: true dan Sunset: <date> untuk tujuan ini. Deprecation menandai bahwa versi akan dihapus; Sunset menentukan tanggal penghapusan.
GET /v0/users
Response:
HTTP/1.1 410 Gone
Deprecation: true
Sunset: Wed, 01 Jun 2026 00:00:00 GMT
{"error":"Gone: v0 sunset 2026-06-01, pakai v3"}Status 410 Gone menandakan resource permanen hilang. Klien yang melihat header ini sebaiknya memigrasi sebelum tanggal Sunset.
Demo Versioning #
Kita akan membangun tiga handler — handleUrlVersioning, handleHeaderVersioning, handleContentNegotiation — yang menerima request dan mengembalikan respons berdasarkan versi yang diminta. Program ini mendemonstrasikan lima skenario.
// api_versioning.kt
// Mendemonstrasikan strategi versioning REST API: URL versioning (/v1/),
// header versioning, content negotiation (Accept header), dan deprecation.
data class UserV1(val id: Int, val nama: String)
data class UserV2(val id: Int, val namaDepan: String, val namaBelakang: String)
data class UserV3(val id: Int, val namaDepan: String, val namaBelakang: String, val email: String)
object UserStore {
val v1 = listOf(UserV1(1, "Budi Santoso"), UserV1(2, "Siti Aminah"))
val v2 = listOf(
UserV2(1, "Budi", "Santoso"),
UserV2(2, "Siti", "Aminah")
)
val v3 = listOf(
UserV3(1, "Budi", "Santoso", "[email protected]"),
UserV3(2, "Siti", "Aminah", "[email protected]")
)
}
data class HttpRequest(
val path: String,
val headers: Map<String, String> = emptyMap()
)
data class HttpResponse(val status: Int, val body: String, val headers: Map<String, String> = emptyMap())
// URL Versioning: /v1/users, /v2/users, /v3/users
fun handleUrlVersioning(req: HttpRequest): HttpResponse {
val path = req.path.trimStart('/')
return when {
path.startsWith("v1/users") -> {
val body = UserStore.v1.joinToString(prefix = "[", postfix = "]") {
"""{"id":${it.id},"nama":"${it.nama}"}"""
}
HttpResponse(200, body, mapOf("X-API-Version" to "v1"))
}
path.startsWith("v2/users") -> {
val body = UserStore.v2.joinToString(prefix = "[", postfix = "]") {
"""{"id":${it.id},"namaDepan":"${it.namaDepan}","namaBelakang":"${it.namaBelakang}"}"""
}
HttpResponse(200, body, mapOf("X-API-Version" to "v2"))
}
path.startsWith("v3/users") -> {
val body = UserStore.v3.joinToString(prefix = "[", postfix = "]") {
"""{"id":${it.id},"namaDepan":"${it.namaDepan}","namaBelakang":"${it.namaBelakang}","email":"${it.email}"}"""
}
HttpResponse(200, body, mapOf("X-API-Version" to "v3"))
}
path.startsWith("v0/users") -> {
// Deprecated: kembalikan data V1 dengan header Deprecation + Sunset
HttpResponse(410, """{"error":"Gone: v0 sunset 2026-06-01, pakai v3"}""",
mapOf("Deprecation" to "true", "Sunset" to "Wed, 01 Jun 2026 00:00:00 GMT"))
}
else -> HttpResponse(404, """{"error":"Not Found"}""")
}
}
// Header Versioning: X-API-Version: 1, 2, 3
fun handleHeaderVersioning(req: HttpRequest): HttpResponse {
val version = req.headers["X-API-Version"] ?: "1"
return when (version) {
"1" -> HttpResponse(200, """{"id":1,"nama":"Budi Santoso"}""", mapOf("X-API-Version" to "1"))
"2" -> HttpResponse(200, """{"id":1,"namaDepan":"Budi","namaBelakang":"Santoso"}""",
mapOf("X-API-Version" to "2"))
"3" -> HttpResponse(200,
"""{"id":1,"namaDepan":"Budi","namaBelakang":"Santoso","email":"[email protected]"}""",
mapOf("X-API-Version" to "3"))
else -> HttpResponse(400, """{"error":"Unknown version $version"}""")
}
}
// Content Negotiation: Accept: application/vnd.example.v3+json
fun handleContentNegotiation(req: HttpRequest): HttpResponse {
val accept = req.headers["Accept"] ?: "application/json"
val version = Regex("""vnd\.example\.v(\d+)\+json""").find(accept)?.groupValues?.get(1)
return when (version) {
"1" -> HttpResponse(200, """{"id":1,"nama":"Budi Santoso"}""",
mapOf("Content-Type" to "application/vnd.example.v1+json"))
"2" -> HttpResponse(200, """{"id":1,"namaDepan":"Budi","namaBelakang":"Santoso"}""",
mapOf("Content-Type" to "application/vnd.example.v2+json"))
"3" -> HttpResponse(200,
"""{"id":1,"namaDepan":"Budi","namaBelakang":"Santoso","email":"[email protected]"}""",
mapOf("Content-Type" to "application/vnd.example.v3+json"))
else -> HttpResponse(200, """{"id":1,"nama":"Budi Santoso"}""",
mapOf("Content-Type" to "application/json"))
}
}
fun main() {
println("=== URL Versioning: /v1/users ===")
val r1 = handleUrlVersioning(HttpRequest("v1/users"))
println(" Status=${r1.status} X-API-Version=${r1.headers["X-API-Version"]}")
println(" Body=${r1.body}")
println("\n=== URL Versioning: /v3/users ===")
val r3 = handleUrlVersioning(HttpRequest("v3/users"))
println(" Status=${r3.status} X-API-Version=${r3.headers["X-API-Version"]}")
println(" Body=${r3.body}")
println("\n=== URL Versioning: /v0/users (deprecated, sunset) ===")
val r0 = handleUrlVersioning(HttpRequest("v0/users"))
println(" Status=${r0.status}")
println(" Deprecation=${r0.headers["Deprecation"]} Sunset=${r0.headers["Sunset"]}")
println(" Body=${r0.body}")
println("\n=== Header Versioning: X-API-Version=2 ===")
val rh2 = handleHeaderVersioning(HttpRequest("/users", mapOf("X-API-Version" to "2")))
println(" Status=${rh2.status} Body=${rh2.body}")
println("\n=== Content Negotiation: Accept=application/vnd.example.v3+json ===")
val rc3 = handleContentNegotiation(HttpRequest("/users",
mapOf("Accept" to "application/vnd.example.v3+json")))
println(" Status=${rc3.status} Content-Type=${rc3.headers["Content-Type"]}")
println(" Body=${rc3.body}")
println("\n=== Content Negotiation: Accept=application/json (default v1) ===")
val rc0 = handleContentNegotiation(HttpRequest("/users",
mapOf("Accept" to "application/json")))
println(" Status=${rc0.status} Content-Type=${rc0.headers["Content-Type"]}")
println(" Body=${rc0.body}")
}Output yang Dihasilkan:
=== URL Versioning: /v1/users ===
Status=200 X-API-Version=v1
Body=[{"id":1,"nama":"Budi Santoso"}, {"id":2,"nama":"Siti Aminah"}]
=== URL Versioning: /v3/users ===
Status=200 X-API-Version=v3
Body=[{"id":1,"namaDepan":"Budi","namaBelakang":"Santoso","email":"[email protected]"}, {"id":2,"namaDepan":"Siti","namaBelakang":"Aminah","email":"[email protected]"}]
=== URL Versioning: /v0/users (deprecated, sunset) ===
Status=410
Deprecation=true Sunset=Wed, 01 Jun 2026 00:00:00 GMT
Body={"error":"Gone: v0 sunset 2026-06-01, pakai v3"}
=== Header Versioning: X-API-Version=2 ===
Status=200 Body={"id":1,"namaDepan":"Budi","namaBelakang":"Santoso"}
=== Content Negotiation: Accept=application/vnd.example.v3+json ===
Status=200 Content-Type=application/vnd.example.v3+json
Body={"id":1,"namaDepan":"Budi","namaBelakang":"Santoso","email":"[email protected]"}
=== Content Negotiation: Accept=application/json (default v1) ===
Status=200 Content-Type=application/json
Body={"id":1,"nama":"Budi Santoso"}Mari kita bedah bagian penting kode di atas:
data class UserV1, UserV2, UserV3: tiga DTO terpisah untuk tiap versi. Fieldnamadi v1 dipecah menjadinamaDepan+namaBelakangdi v2, lalu ditambahemaildi v3. DTO terpisah memastikan tipe safety — klien tidak dapat akses field yang tidak ada di versi mereka.when { path.startsWith("v1/users") -> ... }: pencocokan prefix pada URL. Spring MVC memakai@RequestMapping("/v1/users")untuk pola yang sama — di sini kita lakukan manual.val version = req.headers["X-API-Version"] ?: "1": default versi 1 bila header tidak ada. Default penting untuk backward compatibility — klien lama yang tidak mengirim header tetap mendapat respons v1.Regex("vnd\\.example\\.v(\\d+)\\+json"): regex mengekstrak nomor versi dari media type custom.vnd.exampleadalah vendor prefix sesuai RFC 6838. Versiv3di-capture sebagai group pertama.mapOf("Deprecation" to "true", "Sunset" to "Wed, 01 Jun 2026 ..."): header deprecation yang dibaca klien untuk migrasi. Status 410 Gone menandakan resource permanen dihapus.
Kapan Perlu Versioning #
Tidak semua perubahan memerlukan versi baru. Penambahan field (non-breaking) tidak perlu versi — klien lama tetap dapat memparse respons baru (field tambahan diabaikan bila klien tidak membacanya). Perubahan breaking (menghapus field, mengubah tipe, mengubah semantik) wajib versi baru.
Referensi Lanjutan #
- Ktor routing
— panduan routing Ktor untuk menata prefix versi seperti
/api/v1dan/api/v2 - Spring Boot reference — rujukan Spring Boot untuk strategi versioning berbasis header dan URL
Selanjutnya #
Setelah versioning dipahami, langkah berikutnya adalah dokumentasi API. Chapter API Documentation akan membahas OpenAPI/Swagger dan springdoc-openapi yang menghasilkan dokumentasi otomatis dari kode.