API sürümleme (API versioning), istemcileri bozacak bir değişikliği yapmanız gerektiğinde eski ve yeni sözleşmeyi bir süre yan yana sunmanızı sağlayan düzendir. En yaygın iki yöntem, sürüm numarasını URL yoluna koymak (/v1/kullanicilar, /v2/kullanicilar) ve sürümü Accept başlığında özel bir medya tipiyle istemektir (Accept: application/vnd.myapi.v2+json). Bu serinin dayandığı tasarım kaynağı URL yolu yöntemini önerir; gerekçesi basitlik, kolay yönlendirme ve önbellekte ayrı tutulmadır.

Bu yazıda iki yöntemi aynı kullanıcı kaynağı üzerinde çalışan FastAPI örnekleriyle karşılaştırıyor, başlık yönteminde RFC 9110'un Accept, Vary ve 406 tanımlarının nasıl devreye girdiğini gösteriyoruz. Hangi değişikliğin yeni sürüm gerektirdiği kırıcı değişiklikler yazısının, eski sürümün nasıl emekliye ayrılacağı Deprecation ve Sunset başlıkları yazısının konusudur. Serinin tamamı için REST API tasarımı öğrenme rehberine bakabilirsiniz.

Neden sürümlemeye ihtiyaç duyulur?

Neden sürümlemeye ihtiyaç duyulur?

Bir API yayımlandığı anda, kontrolünüzde olmayan istemciler ona göre yazılmaya başlar: başka ekiplerin servisleri, müşterilerin entegrasyonları, mağazada güncellenmeyi bekleyen mobil uygulama sürümleri. Bir alanı yeniden adlandırmak veya tipini değiştirmek bu istemcilerin hepsini aynı anda bozar. Sürümleme, yeni sözleşmeyi yeni bir adreste ya da yeni bir medya tipinde sunup eskisini bir geçiş süresi boyunca çalışır tutmanın yoludur. Böylece istemciler kendi takvimleriyle geçiş yapabilir.

Önemli bir nokta: sürüm her değişiklikte artmaz. İsteğe bağlı yeni bir alan eklemek gibi geriye uyumlu değişiklikler mevcut sürümün içinde yapılır. Yeni sürüm yalnızca alan silme, yeniden adlandırma, tip değiştirme veya yeni zorunlu alan ekleme gibi kırıcı değişiklikler için açılır. Sürüm numarasını bu yüzden genellikle yalnızca ana sayı olarak tutarsınız: v1, v2. URL'de v1.4.2 gibi ayrıntılı numaralar, her küçük değişiklikte istemcileri adres değiştirmeye zorlar ve sürümlemenin amacına ters düşer.

Yöntem 1: URL yolunda sürüm

Sürüm, kaynak yolunun başına bir önek olarak eklenir. İstemci hangi sözleşmeyi kullandığını adresin kendisinden görür:

http
GET /v1/kullanicilar/7
GET /v2/kullanicilar/7

Tasarım kaynağı bu yöntemin artılarını üç maddede toplar: basit ve açıktır, kolay yönlendirilir (routing) ve önbellekte ayrı tutulur. Basitliği, sürümün log kayıtlarında, hata raporlarında ve belgelerde adresle birlikte görünmesinden gelir; bir istemcinin hangi sürümü çağırdığını anlamak için başka bir şeye bakmanız gerekmez. Yönlendirme kolaylığı, ters vekil sunucu (reverse proxy) veya API ağ geçidi düzeyinde /v1/ ile başlayan istekleri eski uygulamaya, /v2/ ile başlayanları yeni uygulamaya göndermenin yalnızca yola bakan bir kuralla yapılabilmesidir. Önbellek ayrımı ise HTTP önbelleklerinin yanıtları temel olarak hedef URI ile anahtarlamasından gelir: iki sürüm farklı adreslerde olduğu için ek bir başlık gerekmeden ayrı saklanır.

Kaynağın saydığı eksi, URL'nin değişmesidir. Aynı kullanıcı kaynağı artık iki adrese sahiptir ve istemcinin kaydettiği bağlantılar sürüm numarası taşır. Bir kaynağı tek bir kalıcı adresle tanımlama fikrinden bu ölçüde uzaklaşılır; pratikte bu, sürüm geçişlerinde istemcinin adres önekini güncellemesi anlamına gelir.

FastAPI ile URL sürümleme

FastAPI'de her sürüm için bir APIRouter oluşturup prefix parametresine sürüm önekini vermek yeterlidir. İki yönlendirici aynı uygulamaya eklenir:

python
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient

app = FastAPI()
v1 = APIRouter(prefix="/v1", tags=["v1"])
v2 = APIRouter(prefix="/v2", tags=["v2"])

KULLANICI = {"id": 7, "ad": "Ayşe", "soyad": "Demir"}

@v1.get("/kullanicilar/{kid}")
def getir_v1(kid: int):
    return {"id": kid, "name": f'{KULLANICI["ad"]} {KULLANICI["soyad"]}'}

@v2.get("/kullanicilar/{kid}")
def getir_v2(kid: int):
    return {"id": kid, "full_name": f'{KULLANICI["ad"]} {KULLANICI["soyad"]}'}

app.include_router(v1)
app.include_router(v2)

c = TestClient(app)
print(c.get("/v1/kullanicilar/7").json())
# çıktı: {'id': 7, 'name': 'Ayşe Demir'}
print(c.get("/v2/kullanicilar/7").json())
# çıktı: {'id': 7, 'full_name': 'Ayşe Demir'}
print(c.get("/v3/kullanicilar/7").status_code)
# çıktı: 404
print(sorted(app.openapi()["paths"]))
# çıktı: ['/v1/kullanicilar/{kid}', '/v2/kullanicilar/{kid}']

Çıktı üç şeyi gösteriyor. v1 eski name alanını, v2 yeniden adlandırılmış full_name alanını döndürüyor; yani kırıcı değişiklik yalnızca yeni sürümde yaşanıyor. Tanımlanmamış /v3/ öneki için uygulama 404 veriyor, çünkü öyle bir kaynak yok. Son olarak otomatik üretilen OpenAPI belgesinde iki sürümün yolları ayrı ayrı listeleniyor; tags parametresi sayesinde etkileşimli belgede de iki grup hâlinde görünürler. OpenAPI belgelerinin ayrıntısı OpenAPI ve Swagger yazısında anlatılıyor.

Yöntem 2: Accept başlığında sürüm

Başlık sürümlemede (header versioning) kaynak adresi sabit kalır, istemci istediği sürümü özel bir medya tipiyle belirtir:

http
GET /kullanicilar/7 HTTP/1.1
Host: api.example.com
Accept: application/vnd.myapi.v2+json

RFC 9110'a göre medya tipi tür/alt-tür biçimindedir ve isteğe bağlı parametreler alabilir; Accept başlığı da istemcinin yanıt için tercih ettiği medya tiplerini bildirmesine yarar. application/vnd.myapi.v2+json örneğinde alt tür, üreticiye özel (vendor) bir ad, API'nin adı (myapi), sözleşmenin sürümü (v2) ve içeriğin JSON olduğunu hatırlatan +json sonekinden oluşacak biçimde seçilmiştir. Sunucu seçtiği temsili Content-Type başlığında aynı medya tipiyle geri bildirir:

http
HTTP/1.1 200 OK
Content-Type: application/vnd.myapi.v2+json
Vary: Accept

{"id": 7, "full_name": "Ayşe Demir"}

Tasarım kaynağı bu yöntemin artılarını "aynı URL" ve "anlamsal" olarak, eksilerini "test etmesi daha zor, başlık gerektirir" ve "önbellekte ayrı tutulmaz" olarak sayar. Anlamsal olması, sürümün bir kaynağın kimliği değil temsilinin biçimi olduğu görüşüne dayanır: kullanıcı 7 aynı kullanıcıdır, yalnızca JSON şekli değişmiştir.

Önbellek konusunda RFC 9110 önemli bir ayrıntı ekler. Vary başlığı, yanıtın seçilmesinde hangi istek başlıklarının rol oynadığını bildirir ve önbelleklere, listelenen başlıklar aynı değilse bu yanıtı başka bir istek için kullanmamalarını söyler. Yani Vary: Accept gönderirseniz önbellek v1 ve v2 yanıtlarını ayrı tutar. Kaynaktaki "önbellekte ayrı tutulmaz" uyarısı aslında Vary unutulduğunda geçerlidir ve gerçek bir risktir: başlık eksikse bir ara önbellek v2 isteyen istemciye daha önce saklanmış v1 yanıtını verebilir. Önbellek mekanizmasının kendisini Cache-Control ve ETag yazısında anlattık.

FastAPI ile başlık sürümleme

Aşağıdaki örnek, Accept başlığından sürüm numarasını okur, başlık yoksa veya özel medya tipi içermiyorsa v1 varsayar, desteklenmeyen sürümde 406 döndürür ve her yanıta Vary: Accept ekler:

python
import re

from fastapi import FastAPI, Header
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient

app = FastAPI()
DESEN = re.compile(r"application/vnd\.myapi\.v(\d+)\+json")
GOVDE = {1: {"id": 7, "name": "Ayşe Demir"}, 2: {"id": 7, "full_name": "Ayşe Demir"}}

@app.get("/kullanicilar/{kid}")
def getir(kid: int, accept: str = Header("application/json")):
    m = DESEN.search(accept)
    surum = int(m.group(1)) if m else 1  # başlık yoksa varsayılan sürüm
    if surum not in GOVDE:
        return JSONResponse({"detail": "Desteklenen sürümler: v1, v2"}, status_code=406,
                            headers={"Vary": "Accept"})
    return JSONResponse(GOVDE[surum], media_type=f"application/vnd.myapi.v{surum}+json",
                        headers={"Vary": "Accept"})

c = TestClient(app)
for a in ("application/json", "application/vnd.myapi.v2+json", "application/vnd.myapi.v9+json"):
    r = c.get("/kullanicilar/7", headers={"Accept": a})
    print(r.status_code, r.headers["content-type"], r.headers["vary"], r.json())
# çıktı: 200 application/vnd.myapi.v1+json Accept {'id': 7, 'name': 'Ayşe Demir'}
# çıktı: 200 application/vnd.myapi.v2+json Accept {'id': 7, 'full_name': 'Ayşe Demir'}
# çıktı: 406 application/json Accept {'detail': 'Desteklenen sürümler: v1, v2'}

406 Not Acceptable, RFC 9110'da hedef kaynağın istemcinin proaktif içerik anlaşması başlıklarına uygun bir temsili olmadığını ve sunucunun varsayılan bir temsil vermek istemediğini anlatan koddur. RFC, bu yanıtta mevcut seçeneklerin listesini vermeyi önerir; örnekteki detail mesajı bunu basitçe yapıyor. Alternatif olarak sunucu bilinmeyen sürümde varsayılan sürüme düşebilir; bu daha hoşgörülü görünse de istemcinin yanlış yazdığı bir sürüm numarasını fark etmesini zorlaştırır. Hangisini seçerseniz belgeye yazın. Genel içerik anlaşması ve 415 kodu content negotiation yazısında ayrıca ele alınıyor.

İki yöntemin karşılaştırması

İki yöntemin karşılaştırması: Görünürlük, Tarayıcı ve curl ile deneme, Yönlendirme, Önbellek
ÖlçütURL yolu (/v1/)Accept başlığı (vnd...v1+json)
GörünürlükAdreste, loglarda açıkça görünürYalnızca başlıkta görünür
Tarayıcı ve curl ile denemeAdresi yazmak yeterliBaşlık eklemek gerekir
YönlendirmeYol önekine bakan basit kuralBaşlığı okuyan kural gerekir
ÖnbellekFarklı URI olduğu için ayrıVary: Accept ile ayrı, unutulursa karışır
Kaynak adresiSürüm başına değişirSabit kalır
Bilinmeyen sürüm404 (yol yok)406 veya varsayılana düşme
BelgelemeYollar ayrı listelenirMedya tipleri ayrıca anlatılmalı

Deneme kolaylığı farkı en çok günlük kullanımda hissedilir. URL sürümünü bir tarayıcı adres çubuğunda ya da tek satırlık bir curl komutunda görebilirsiniz; başlık sürümü ise her istekte ek bir parametre gerektirir:

bash
# URL sürümleme: adres çubuğuna veya curl'e yazmak yeterli
curl https://api.example.com/v2/kullanicilar/7

# Başlık sürümleme: başlığı her istekte eklemek gerekir
curl -H "Accept: application/vnd.myapi.v2+json" https://api.example.com/kullanicilar/7

Bir sürüm neyi kapsar?

Sürüm numarası yalnızca JSON alan adlarının değil, istemcinin dayandığı tüm sözleşmenin etiketidir. Bir v1 istemcisi alan adlarına ve tiplerine olduğu kadar, hangi durumda hangi durum kodunun döneceğine, hata gövdesinin şekline, sayfalama parametrelerinin adına ve varsayılan sayfa boyutuna, kimlik doğrulama başlığının biçimine de güvenir. Örneğin v1 içinde hata yanıtlarını bir gün özel bir hata biçiminden başka bir biçime geçirmek ya da varsayılan sıralamayı değiştirmek, alan adı değiştirmek kadar kırıcı olabilir. Bu yüzden sürüm planlarken "bu değişiklik v1 istemcisinin yazdığı herhangi bir varsayımı bozar mı?" sorusunu sormak, yalnızca JSON şemasına bakmaktan daha güvenilir bir ölçüttür.

Bu bakış açısı, sürümlerin ortak kodu nasıl paylaşacağını da belirler. Veritabanı ve iş kuralları tek olmalı; v1 ve v2 arasında farklılaşan şey yalnızca dışarıya verilen temsil, kabul edilen istek gövdesi ve belgelenmiş davranış olmalıdır. İki sürümü iki ayrı kod tabanı gibi büyütmek, bir hatanın birinde düzeltilip diğerinde unutulmasına zemin hazırlar.

Yanıtta sürüm bilgisi gerekir mi?

URL sürümlemede istemci sürümü zaten adreste belirtir, bu yüzden yanıt gövdesine ayrıca sürüm alanı koymak zorunlu değildir. Tasarım kaynağındaki kısa örnek, iki sürümün yanıtına "version": 1 ve "version": 2 alanlarını ekleyerek farkı görünür kılar; bu, hata ayıklarken hangi uygulamanın yanıt verdiğini doğrulamak için işe yarayabilir. Başlık sürümlemede ise yanıttaki Content-Type başlığı bu görevi üstlenir: sunucu hangi sürümle yanıt verdiyse o medya tipini döndürür ve istemci bunu kontrol edebilir. Hangi yolu seçerseniz seçin, sürüm bilgisini sözleşmenin parçası yaptıysanız sonradan kaldırmanın da bir değişiklik olduğunu unutmayın.

Hangisini seçmeli?

Herkese açık, birçok farklı istemcinin kullandığı ve basit araçlarla denenmesi gereken bir API için URL yolu genellikle daha az sürpriz üretir; tasarım kaynağının bu yöntemi önermesinin nedeni de budur. Kaynak adresinin kalıcı olmasının önemli olduğu, istemcilerin HTTP başlıklarını rahatça yönettiği ve içerik anlaşmasını zaten kullanan sistemlerde başlık yöntemi daha tutarlı bir model sunabilir. Bu bir standart meselesi değil, bir tasarım tercihidir; HTTP iki yöntemi de destekler. Asıl önemli olan tek bir yöntem seçip tüm API'de aynı biçimde uygulamaktır.

Seçiminizden bağımsız olarak üç kural geçerlidir. Birincisi, sürüm API'nin tamamına uygulanır; bir uç noktanın v2'de, diğerinin v1'de kalması doğal olabilir ama istemciler için her uç noktanın hangi sürümde olduğu belgede net olmalıdır. İkincisi, aynı anda çalışan sürüm sayısını küçük tutun; her eski sürüm bakım, test ve güvenlik güncellemesi demektir. Üçüncüsü, yeni sürümü yayımladığınız gün eski sürümün emeklilik planını da duyurun; RFC 8594 tanımlı Sunset başlığı ve ilgili yöntemler deprecation yazısında anlatılıyor.

Sık yapılan hatalar

Sık yapılan hatalar: Her değişiklikte yeni sürüm, Kırıcı değişikliği mevcut sürüme koymak, Sürümü yolun ortasına koymak, Ayrıntılı sürüm numarası
  • Her değişiklikte yeni sürüm: İsteğe bağlı alan eklemek için v2 açmak, istemcileri gereksiz geçişlere zorlar.
  • Kırıcı değişikliği mevcut sürüme koymak: v1 içinde alan adını değiştirmek, sürümlemenin varlık nedenini ortadan kaldırır.
  • Sürümü yolun ortasına koymak: /kullanicilar/v1/7 gibi yollar yönlendirmeyi ve okumayı zorlaştırır; önek olarak başta tutun.
  • Ayrıntılı sürüm numarası: /v1.4.2/ gibi değerler her küçük sürümde adresi değiştirir.
  • Başlık sürümlemede Vary unutmak: Ara önbellekler farklı sürümlerin yanıtlarını karıştırabilir.
  • Varsayılan sürümü en yeni sürüme bağlamak: Başlık göndermeyen istemciler yeni sürüm günü bozulur.
  • İki yöntemi karıştırmak: Hem URL'de hem başlıkta sürüm kabul etmek, ikisi çeliştiğinde hangisinin geçerli olduğu sorusunu doğurur.

Sık Sorulan Sorular

URL sürümleme REST ilkelerine aykırı mı?

Kaynağın kimliğinin sürümden bağımsız olması gerektiğini savunanlar başlık yöntemini daha "anlamsal" bulur. Ancak URL sürümleme yaygın ve pratik bir tercihtir; tasarım kaynağımız da onu önerir. Burada kesin bir kural yoktur, tutarlılık önemlidir.

Sürüm numarası v1 ile mi başlamalı, v0 ile mi?

Bir zorunluluk yoktur. Yayımlanmış ve istemcilere söz verilmiş ilk sözleşmeyi v1 olarak adlandırmak yaygındır; böylece "kararlı" ile "deneme" arasındaki ayrım sürüm numarasından okunur.

Sürümü sorgu parametresiyle vermek olur mu?

Bu serinin kaynakları yalnızca URL yolu ve başlık yöntemlerini ele alır. Sorgu parametresi genellikle filtreleme ve sıralama gibi görünüm tercihleri için kullanılır; ayrımı path ve query parametresi yazısında anlattık.

Eski sürümü ne zaman kapatmalıyım?

Bir standart süre yoktur. Kapanış tarihini önceden duyurun, yanıtlarda Deprecation ve Sunset başlıklarıyla bildirin ve istemcilerin geçiş yapmasına yetecek bir süre bırakın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: REST API tasarımı öğrenme rehberi · Breaking change ve geriye uyumluluk · Deprecation ve Sunset başlıkları · Content negotiation ve Accept · OpenAPI ve Swagger ile dokümantasyon