API Versioning
Giriş — Neden Bu Konu Önemli?
API'ler zamanla değişir — yeni alanlar eklenir, eski alanlar kaldırılır, iş kuralları güncellenir. Ancak mevcut istemcilerin (mobil uygulamalar, üçüncü parti entegrasyonlar) bozulmaması gerekir. Bir mobil uygulama güncelleme almayabilir — app store'da hâlâ eski versiyon çalışıyor olabilir. İşte bu noktada API versiyonlama devreye girer.
Versiyonlama, eski istemcilerin çalışmaya devam etmesini sağlarken yeni istemcilere güncel yapıyı sunar. Doğru versiyonlama stratejisi seçimi, API'nizin uzun vadeli sürdürülebilirliği için kritiktir.
Gerçek Hayat Analojisi: Bir yazılımın v1.0 ve v2.0'ını düşünün. v2.0'da arayüz tamamen değişti ama v1.0 kullanıcıları hâlâ eski arayüzü kullanmak istiyor. İki versiyonu aynı anda desteklemek zorundasınız — bu, API versiyonlama ile aynı problemdir. Soru şu: versiyon bilgisini nerede taşıyacaksınız? URL'de mi, header'da mı, yoksa başka bir yerde mi?
Neden Versiyonlama Gerekli?
Bir e-ticaret API'niz olduğunu düşünün. İlk versiyonda kullanıcı yanıtı şöyle:
{
"name": "Ali Yılmaz",
"email": "ali@example.com"
}Sonra name alanını firstName ve lastName olarak ayırmanız gerekti:
{
"firstName": "Ali",
"lastName": "Yılmaz",
"email": "ali@example.com"
}Eğer bu değişikliği doğrudan yaparsanız, eski name alanını kullanan tüm istemciler bozulur. Mobil app crash eder, partner entegrasyonları çöker, müşteri şikayetleri yağar.
Breaking vs Non-Breaking Changes
| Değişiklik | Breaking? | Açıklama |
|---|---|---|
| Yeni alan ekleme | ❌ | İstemci bilmediği alanları görmezden gelir |
| Zorunlu alan ekleme (request) | ✅ | Eski istemciler bu alanı göndermez |
| Alan kaldırma | ✅ | İstemci bu alanı bekler |
| Alan adı değiştirme | ✅ | İstemci eski ismi kullanır |
| Veri tipi değiştirme | ✅ | String → Number dönüşümü |
| Endpoint kaldırma | ✅ | İstemci bu endpoint'e istek atar |
| Yeni endpoint ekleme | ❌ | Eski istemciler bilmez, sorun olmaz |
| Status code değiştirme | ✅ | İstemci belirli kodu bekler |
Kural: Non-breaking change'ler versiyonlama gerektirmez. Breaking change = yeni versiyon.
Strateji 1: URI Versioning (URL Path)
En yaygın ve en basit yaklaşımdır. Versiyon numarası URL'nin bir parçası olarak belirtilir:
GET /api/v1/users/42 → eski format (name)
GET /api/v2/users/42 → yeni format (firstName, lastName)Spring Boot Uygulaması
// V1 Controller
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserV1Dto> getUser(@PathVariable Long id) {
User user = userService.findById(id);
UserV1Dto dto = new UserV1Dto(user.getFullName(), user.getEmail());
return ResponseEntity.ok(dto);
}
@GetMapping
public ResponseEntity<List<UserV1Dto>> getAllUsers() {
List<UserV1Dto> users = userService.findAll().stream()
.map(u -> new UserV1Dto(u.getFullName(), u.getEmail()))
.toList();
return ResponseEntity.ok(users);
}
}
// V2 Controller
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserV2Dto> getUser(@PathVariable Long id) {
User user = userService.findById(id);
UserV2Dto dto = new UserV2Dto(
user.getFirstName(), user.getLastName(), user.getEmail());
return ResponseEntity.ok(dto);
}
}
// DTO'lar
public record UserV1Dto(String name, String email) {}
public record UserV2Dto(String firstName, String lastName, String email) {}Avantajları:
Açık ve anlaşılır — URL'den versiyon belli
Cache-friendly — her URL farklı resource
Tarayıcıdan test edilebilir
Load balancer / API Gateway seviyesinde yönlendirme yapılabilir
Dezavantajları:
URL kirliliği yaratır
Controller sınıflarının kopyalanması gerekebilir
Kaynak sayısı arttıkça yönetimi zorlaşır
💡 Kim kullanıyor? GitHub (
/v3/repos), Twitter (/2/tweets), Google Maps (/v1/directions), Stripe (/v1/charges). Endüstri standardı sayılır.
Strateji 2: Request Parameter Versioning
Versiyon bilgisi query parameter olarak gönderilir:
GET /api/users/42?version=1
GET /api/users/42?version=2Spring Boot Uygulaması
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@GetMapping(value = "/{id}", params = "version=1")
public ResponseEntity<UserV1Dto> getUserV1(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(
new UserV1Dto(user.getFullName(), user.getEmail()));
}
@GetMapping(value = "/{id}", params = "version=2")
public ResponseEntity<UserV2Dto> getUserV2(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(new UserV2Dto(
user.getFirstName(), user.getLastName(), user.getEmail()));
}
// Default versiyon — parametre yoksa en son versiyonu döndür
@GetMapping(value = "/{id}", params = "!version")
public ResponseEntity<UserV2Dto> getUserLatest(@PathVariable Long id) {
return getUserV2(id);
}
}Avantajları: Tek controller sınıfı yeterlidir, URL'ler temiz kalır. Dezavantajları: Cache mekanizmalarıyla uyumsuzluk olabilir (query param cache key'e dahil edilmeyebilir), parameter unutulursa hangi versiyonun döneceği belirsizdir.
Strateji 3: Custom Header Versioning
Versiyon bilgisi özel bir HTTP header ile gönderilir:
GET /api/users/42
X-API-Version: 1
GET /api/users/42
X-API-Version: 2Spring Boot Uygulaması
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}", headers = "X-API-Version=1")
public ResponseEntity<UserV1Dto> getUserV1(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(
new UserV1Dto(user.getFullName(), user.getEmail()));
}
@GetMapping(value = "/{id}", headers = "X-API-Version=2")
public ResponseEntity<UserV2Dto> getUserV2(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(new UserV2Dto(
user.getFirstName(), user.getLastName(), user.getEmail()));
}
}Avantajları: URL'ler tamamen temiz kalır, versiyon bilgisi request metadata'sında taşınır. Dezavantajları: Tarayıcıdan test etmek zordur (Postman/curl gerektirir), API keşfedilebilirliği düşer.
Strateji 4: Content Negotiation (Media Type Versioning)
Accept header kullanılarak versiyon bilgisi media type içinde taşınır. Bu yaklaşım en "RESTful" olanıdır çünkü HTTP'nin content negotiation mekanizmasını kullanır:
GET /api/users/42
Accept: application/vnd.myapi.v1+json
GET /api/users/42
Accept: application/vnd.myapi.v2+jsonSpring Boot Uygulaması
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}",
produces = "application/vnd.myapi.v1+json")
public ResponseEntity<UserV1Dto> getUserV1(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(
new UserV1Dto(user.getFullName(), user.getEmail()));
}
@GetMapping(value = "/{id}",
produces = "application/vnd.myapi.v2+json")
public ResponseEntity<UserV2Dto> getUserV2(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(new UserV2Dto(
user.getFirstName(), user.getLastName(), user.getEmail()));
}
}Avantajları: En RESTful yaklaşım, URL'ler tamamen temiz, HTTP standartlarına uygun. Dezavantajları: En karmaşık yaklaşım, test etmesi zordur, istemci tarafında ek yapılandırma gerektirir.
💡 Kim kullanıyor? GitHub API bu yaklaşımı kullanır:
Accept: application/vnd.github.v3+json
Strateji Karşılaştırması
| Kriter | URI | Query Param | Header | Media Type |
|---|---|---|---|---|
| Basitlik | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐ |
| Cache uyumu | ⭐⭐⭐ | ⭐ | ⭐⭐ | ⭐⭐ |
| URL temizliği | ⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Keşfedilebilirlik | ⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐ |
| RESTful uyum | ⭐ | ⭐ | ⭐⭐ | ⭐⭐⭐ |
| Yaygınlık | ⭐⭐⭐ | ⭐ | ⭐⭐ | ⭐ |
Kod Tekrarını Azaltma — Ortak Service
Farklı versiyonlar aynı service'i kullanır, sadece DTO mapping farklıdır:
// Tek service — iş mantığı burada
@Service
public class UserService {
public User findById(Long id) { ... }
public User create(CreateUserRequest request) { ... }
public User update(Long id, UpdateUserRequest request) { ... }
}
// V1 Mapper
@Component
public class UserMapperV1 {
public UserV1Dto toDto(User user) {
return new UserV1Dto(user.getFullName(), user.getEmail());
}
}
// V2 Mapper
@Component
public class UserMapperV2 {
public UserV2Dto toDto(User user) {
return new UserV2Dto(
user.getFirstName(), user.getLastName(),
user.getEmail(), user.getPhone());
}
}
// V1 Controller — sadece mapping farklı
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
private final UserService userService;
private final UserMapperV1 mapper;
@GetMapping("/{id}")
public ResponseEntity<UserV1Dto> getUser(@PathVariable Long id) {
return ResponseEntity.ok(mapper.toDto(userService.findById(id)));
}
}
// V2 Controller
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
private final UserService userService;
private final UserMapperV2 mapper;
@GetMapping("/{id}")
public ResponseEntity<UserV2Dto> getUser(@PathVariable Long id) {
return ResponseEntity.ok(mapper.toDto(userService.findById(id)));
}
}Versiyon Yönetimi Best Practices
1. Deprecation Policy
// Eski versiyonu deprecated olarak işaretle
@GetMapping("/{id}")
public ResponseEntity<UserV1Dto> getUserV1(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok()
.header("Deprecation", "true")
.header("Sunset", "Sat, 01 Jun 2025 00:00:00 GMT")
.header("Link", "</api/v2/users/" + id + ">; rel=\"successor-version\"")
.body(new UserV1Dto(user.getFullName(), user.getEmail()));
}Deprecation:
trueise bu versiyon kullanımdan kaldırılacakSunset: Bu versiyon bu tarihte kapatılacak
Link: Yeni versiyonun URL'i
2. Minimum Versiyon Sayısı
En fazla 2-3 aktif versiyon tutun. Daha fazlası bakım yükünü katlar.
3. Semantic Versioning
Major (v1 → v2): Breaking change — yeni versiyon
Minor: Non-breaking ekleme — versiyonlama gerekmez
Patch: Bug fix — versiyonlama gerekmez
4. Versiyon Atlama Yapmayın
v1 → v2 → v3 sırasıyla gidin. v1 → v3 atlamayın.
5. Default Versiyon Stratejisi
// Versiyon belirtilmezse en son versiyonu döndür
@GetMapping("/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id,
@RequestHeader(value = "X-API-Version", defaultValue = "2") int version) {
User user = userService.findById(id);
return switch (version) {
case 1 -> ResponseEntity.ok(new UserV1Dto(user.getFullName(), user.getEmail()));
case 2 -> ResponseEntity.ok(new UserV2Dto(
user.getFirstName(), user.getLastName(), user.getEmail()));
default -> ResponseEntity.badRequest()
.body(Map.of("error", "Unsupported API version: " + version));
};
}Hangi Stratejiyi Seçmeli?
URI versioning çoğu proje için en iyi başlangıç noktasıdır:
Basit ve anlaşılır — yeni geliştiriciler hemen kavrar
Cache-friendly — CDN ve proxy'ler URL bazlı cache yapar
Endüstri standardı — GitHub, Twitter, Google Maps bu yaklaşımı kullanır
API Gateway desteği — URL pattern ile routing kolay
Eğer daha sofistike bir yapı gerekiyorsa ve istemcileriniz üzerinde kontrolünüz varsa (internal API), header versioning ikinci tercih olabilir.
Custom Annotation ile Versiyonlama
Daha DRY bir yaklaşım — özel annotation ile versiyon kontrolü:
// Custom annotation
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
int value();
}RequestMappingHandlerMapping Özelleştirmesi
public class ApiVersionRequestMappingHandlerMapping
extends RequestMappingHandlerMapping {
@Override
protected RequestMappingInfo getMappingForMethod(Method method,
Class<?> handlerType) {
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
if (info == null) return null;
ApiVersion methodAnnotation = AnnotatedElementUtils
.findMergedAnnotation(method, ApiVersion.class);
if (methodAnnotation != null) {
info = createVersionInfo(methodAnnotation.value()).combine(info);
} else {
ApiVersion typeAnnotation = AnnotatedElementUtils
.findMergedAnnotation(handlerType, ApiVersion.class);
if (typeAnnotation != null) {
info = createVersionInfo(typeAnnotation.value()).combine(info);
}
}
return info;
}
private RequestMappingInfo createVersionInfo(int version) {
return RequestMappingInfo
.paths("/api/v" + version)
.build();
}
}
// Konfigürasyon
@Configuration
public class WebConfig implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}Kullanım
// Controller'da sadece @ApiVersion eklemek yeterli
@RestController
@RequestMapping("/users")
@ApiVersion(1)
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1Dto getUser(@PathVariable Long id) {
// → GET /api/v1/users/{id} olarak erişilir
return ...;
}
}
@RestController
@RequestMapping("/users")
@ApiVersion(2)
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2Dto getUser(@PathVariable Long id) {
// → GET /api/v2/users/{id} olarak erişilir
return ...;
}
}Bu yaklaşımla her controller'a @RequestMapping("/api/v1/...") yazmak zorunda kalmaz, sadece @ApiVersion(1) yeterli olur.
API Gateway ile Versiyonlama
Microservice mimarisinde versiyonlama genellikle API Gateway seviyesinde yapılır:
# Spring Cloud Gateway konfigürasyonu
spring:
cloud:
gateway:
routes:
- id: users-v1
uri: lb://user-service-v1
predicates:
- Path=/api/v1/users/**
- id: users-v2
uri: lb://user-service-v2
predicates:
- Path=/api/v2/users/**
# Header tabanlı routing
- id: users-header-v1
uri: lb://user-service-v1
predicates:
- Path=/api/users/**
- Header=X-API-Version, 1
- id: users-header-v2
uri: lb://user-service-v2
predicates:
- Path=/api/users/**
- Header=X-API-Version, 2Bu yaklaşımda her versiyon ayrı bir microservice olarak deploy edilir. Gateway, versiyon bilgisine göre doğru servise yönlendirir. Avantajı: eski versiyon bağımsız olarak scale edilebilir ve kapatılabilir.
Versiyon Migration Stratejileri
Eski versiyondan yenisine geçişte kullanılan stratejiler:
1. Adapter Pattern
// V1 Controller, V2 Service'i kullanır ama V1 formatında döner
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
private final UserServiceV2 userService; // En güncel service
@GetMapping("/{id}")
public UserV1Dto getUser(@PathVariable Long id) {
UserV2Dto v2 = userService.findById(id);
// V2 → V1 dönüşümü
return new UserV1Dto(
v2.firstName() + " " + v2.lastName(),
v2.email()
);
}
}2. Backward Compatible Extension
// V1 yanıtını bozmadan yeni alanlar ekle
// Eski istemciler bilmedikleri alanları görmezden gelir
{
"name": "Ali Yılmaz", // V1 — korunuyor
"email": "ali@example.com", // V1 — korunuyor
"firstName": "Ali", // V2 — ek alan
"lastName": "Yılmaz" // V2 — ek alan
}
// Her iki versiyon da aynı endpoint'ten çalışır — versiyonlama gerekmez!3. Canary Release
Yeni versiyonu önce küçük bir kullanıcı grubuna sunup test etme:
# API Gateway canary konfigürasyonu
spring:
cloud:
gateway:
routes:
- id: users-canary
uri: lb://user-service-v2
predicates:
- Path=/api/users/**
- Weight=users, 10 # %10 trafik v2'ye
- id: users-stable
uri: lb://user-service-v1
predicates:
- Path=/api/users/**
- Weight=users, 90 # %90 trafik v1'eÖzet
API versiyonlama, breaking change'lerde eski istemcilerin çalışmaya devam etmesini sağlar
Dört strateji: URI (en yaygın), Query Parameter, Custom Header, Content Negotiation (en RESTful)
URI versioning çoğu proje için en iyi seçimdir — basit, anlaşılır, cache-friendly
Kod tekrarını azaltın — service/mapper pattern ile iş mantığını tek yerde tutun
Deprecation policy belirleyin — Sunset header ile eski versiyonun kapanacağı tarihi bildirin
En fazla 2-3 aktif versiyon tutun — daha fazlası bakım yükü yaratır
Breaking change = yeni versiyon, non-breaking change = versiyon gerekmez
AI Asistan
Sorularını yanıtlamaya hazır