Skip to content
~/sph.sh

API Versioning Stratejileri: İlk Release'den Sunset'a Kadar

URL ve header versioning yaklaşımları, breaking change yönetimi, Sunset header'ları ile deprecation, AWS API Gateway pattern'leri, GraphQL schema evolution ve consumer-driven contract testing'i kapsayan kapsamlı bir API versioning rehberi.

API versioning sadece URL'lerine /v2/ eklemekten ibaret değil. Farklı takımlarla API'ler üzerinde çalışmak bana versioning stratejisinin client migration timeline'larından infrastructure maliyetlerine kadar her şeyi etkilediğini öğretti. Production sistemleri bozmadan API evolution'ı yönetme konusunda öğrendiklerimi paylaşıyorum.

Özet

API versioning, backward compatibility ile forward progress arasında denge kurmayı gerektirir. Bu rehber, URL path ve header versioning karşılaştırması, OpenAPI diff tool'ları ile breaking change yönetimi, RFC 8594 deprecation header'larının implementasyonu, Lambda alias'ları ile AWS API Gateway versioning pattern'leri, traditional versioning olmadan GraphQL schema evolution, Pact ile consumer-driven contract testing ve koordineli migration pattern'lerini kapsar. Yaklaşım, continuous API improvement'ı sağlarken disruption'ı minimize etmek için gradual rollout'lar, comprehensive monitoring ve clear communication'a odaklanır.

Teknik Challenge

API evolution birkaç birbirine bağlı problem yaratır:

Breaking Change Management: Bir field'ı name'den fullName'e rename ettiğinde, name bekleyen mevcut client'lar fail olur. Soru breaking change'lerin yapılıp yapılmaması değil; production incident'larına neden olmadan nasıl yapılacağıdır.

Version Proliferation: Sunset policy olmadığı için altı concurrent API version'ı destekleyen takımlar gördüm. Her version testing matrix'ini, security patch burden'ını ve infrastructure maliyetlerini katlar. Engineering time hızla birikiyor.

Migration Coordination: API'nize 50 farklı client depend ettiğinde, zero-downtime migration'ları coordinate etmek complex hale geliyor. Bazı client'lar hemen update oluyor, diğerleri aylar alıyor. Her ikisini de accommodate eden bir stratejiye ihtiyacın var.

Documentation Synchronization: Birden fazla API version'ında OpenAPI spec'leri, SDK version'larını ve documentation'ı maintain etmek, birçok versioning stratejisinin başarısız olduğu nokta. Doc'lar gerçeklikten uzaklaşıyor, integration confusion'a neden oluyor.

Versioning Stratejini Seçmek

Üç ana yaklaşım var, her birinin spesifik trade-off'ları mevcut:

URL Path Versioning

typescript
// Version URL path'ine gömülüapp.get('/api/v1/users/:id', async (req, res) => {  const user = await db.users.findById(req.params.id);  res.json({    id: user.id,    name: user.name,    email: user.email  });});
app.get('/api/v2/users/:id', async (req, res) => {  const user = await db.users.findById(req.params.id);  res.json({    id: user.id,    fullName: user.name, // Field rename edildi    contactInfo: {      email: user.email,      phone: user.phone    }  });});

Avantajlar: URL'lerde explicit versioning, browser'da test etmesi straightforward, excellent CDN cache efficiency (farklı URL'ler = ayrı cache key'ler).

Dezavantajlar: URL change'leri bookmark'ları ve hardcoded client'ları bozar, her version için routing configuration gerekir.

Ne zaman kullanılır: Clarity'nin URL aesthetic'ten daha önemli olduğu, multiple major version'lı public API'ler için. Twitter, Stripe ve GitHub (historically) bu yaklaşımı kullanır.

Header-Based Versioning

typescript
// Version request header'ından belirlenirapp.use((req, res, next) => {  const apiVersion = req.headers['api-version'] ||                     req.headers['accept-version'] ||                     '1'; // default version
  req.apiVersion = apiVersion;  res.setHeader('API-Version', apiVersion);  next();});
app.get('/api/users/:id', async (req, res) => {  const user = await db.users.findById(req.params.id);
  if (req.apiVersion === '2') {    return res.json(transformToV2(user));  }
  res.json(transformToV1(user));});

Avantajlar: Değişmeyen clean URL'ler, granular version control, content negotiation pattern'lerini destekler.

Dezavantajlar: API client'lar olmadan test etmesi daha zor, log'larda header'ları specifically log etmediğin sürece version invisible, cache configuration Vary header setup gerektirir.

Ne zaman kullanılır: Internal API'ler, frequent minor update'li API'ler, URL stability'nin önemli olduğu sistemler için. GitHub (current approach) ve Microsoft Graph API bu pattern'i kullanır.

Decision Framework

Seçim specific context'ine bağlı. External partner'lı public REST API için URL path versioning clarity sağlar. Internal microservice'ler için header versioning flexibility sunar. Tight consumer-provider coupling'li takımlar için GraphQL evolution modeli versioning complexity'sini elimine eder.

Breaking vs Non-Breaking Change'ler

Breaking change'in ne olduğunu anlamak accidental production incident'ları önler:

Non-Breaking (Güvenli) Change'ler:

  • Yeni endpoint'ler eklemek
  • Optional request parameter'lar eklemek
  • Response'lara yeni field'lar eklemek (existing client'lar ignore eder)
  • Mevcut code'ları valid tutarken yeni response status code'ları eklemek
  • Validation rule'larını relax etmek (daha fazla input format kabul etmek)

Breaking Change'ler (Yeni Version Gerektirir):

  • Endpoint'leri remove veya rename etmek
  • Request/response field'larını remove veya rename etmek
  • Field data type'larını değiştirmek (string'den number'a)
  • Required request parameter eklemek
  • Authentication mechanism'larını değiştirmek
  • Error response structure'larını modify etmek
  • HTTP method'ları değiştirmek (GET'ten POST'a)

Practice'te breaking olmadan evolution şöyle görünür:

typescript
// Original API (v1) - değişmeden kalırinterface UserV1 {  id: string;  name: string;  email: string;}
// Evolved API (v2) - sadece additive change'lerinterface UserV2 {  id: string;  name: string; // compatibility için tutuldu  email: string; // compatibility için tutuldu
  // Yeni optional field'lar  phoneNumber?: string;  avatar?: string;  preferences?: UserPreferences;}
// V2 data'yı gerektiğinde v1 format'a transform etfunction toV1Format(user: UserV2): UserV1 {  return {    id: user.id,    name: user.name,    email: user.email  };}

Automated detection hataları önler. CI/CD pipeline'larda:

typescript
import { diff } from 'openapi-diff';
async function detectBreakingChanges(  oldSpecPath: string,  newSpecPath: string): Promise<void> {  const result = await diff(oldSpecPath, newSpecPath);
  if (result.breakingDifferencesFound) {    console.error('Breaking change tespit edildi:');    result.breakingDifferences.forEach(change => {      console.error(`- ${change.type}: ${change.action}`);      console.error(`  Path: ${change.path}`);    });    process.exit(1); // Build'i fail et  }}

Deprecation'ı Doğru Implement Etmek

Deprecation bir event değil; bir process'tir. Realistic timeline:

Timeline'ı programmatically communicate etmek için RFC 8594 deprecation header'larını implement et:

typescript
interface DeprecationConfig {  version: string;  deprecationDate: Date;  sunsetDate: Date;  migrationGuideUrl: string;}
const v1Config: DeprecationConfig = {  version: '1',  deprecationDate: new Date('2025-07-01'),  sunsetDate: new Date('2026-01-01'),  migrationGuideUrl: 'https://docs.example.com/api/v1-to-v2-migration'};
function addDeprecationHeaders(  res: Response,  config: DeprecationConfig): void {  const now = new Date();
  // RFC 9745 Deprecation header  if (now >= config.deprecationDate) {    res.setHeader('Deprecation', '@' + Math.floor(config.deprecationDate.getTime() / 1000));  }
  // RFC 8594 Sunset header  res.setHeader('Sunset', config.sunsetDate.toUTCString());
  // Migration documentation'a link  res.setHeader('Link',    `<${config.migrationGuideUrl}>; rel="deprecation"; type="text/html"`  );
  // Kalan günlerle warning header  const daysUntilSunset = Math.floor(    (config.sunsetDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)  );
  res.setHeader('Warning',    `299 - "API version ${config.version} ${daysUntilSunset} gün içinde sunset olacak. ` +    `Lütfen v2'ye migrate edin. Bakın: ${config.migrationGuideUrl}"`  );}
app.use('/api/v1/*', (req, res, next) => {  addDeprecationHeaders(res, v1Config);  next();});

Client SDK'lar deprecation'ı detect edip warn etmeli:

typescript
class ApiClient {  private checkDeprecationHeaders(response: Response): void {    const deprecation = response.headers.get('Deprecation');    const sunset = response.headers.get('Sunset');
    if (deprecation) {      const sunsetDate = sunset ? new Date(sunset) : null;      const daysUntilSunset = sunsetDate        ? Math.floor((sunsetDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24))        : null;
      console.warn(        `[API Deprecation Uyarısı] Bu endpoint deprecated.`,        sunsetDate ? `Sunset: ${sunsetDate.toISOString()}` : '',        daysUntilSunset !== null ? `Kalan gün: ${daysUntilSunset}` : ''      );
      // Monitoring'e report et      this.reportDeprecationMetric({        endpoint: response.url,        daysUntilSunset      });    }  }}

Hard shutdown yerine gradual throttling migration panic'ini azaltır:

typescript
function getVersionThrottleLimit(version: string, sunsetDate: Date): number {  const daysUntilSunset = Math.floor(    (sunsetDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)  );
  if (daysUntilSunset > 30) {    return 10000; // Normal rate limit  } else if (daysUntilSunset > 7) {    return 1000; // Reduced rate  } else if (daysUntilSunset > 0) {    return 100; // Severe throttling  } else {    return 0; // Sunset geçti  }}

AWS API Gateway Versioning Pattern'leri

AWS API Gateway birkaç versioning yaklaşımı sunar. Production'da çalışanlar:

Custom Domain ile Base Path Mapping

typescript
import * as apigateway from 'aws-cdk-lib/aws-apigateway';import * as lambda from 'aws-cdk-lib/aws-lambda';import * as acm from 'aws-cdk-lib/aws-certificatemanager';import * as cdk from 'aws-cdk-lib';
export class ApiVersioningStack extends cdk.Stack {  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {    super(scope, id, props);
    // Versioning'li Lambda function    const userServiceFn = new lambda.Function(this, 'UserService', {      runtime: lambda.Runtime.NODEJS_20_X,      handler: 'index.handler',      code: lambda.Code.fromAsset('lambda'),    });
    // Version 1'e point eden v1 alias    const v1Alias = new lambda.Alias(this, 'UserServiceV1', {      aliasName: 'v1',      version: userServiceFn.currentVersion,    });
    // Version 2'ye point eden v2 alias    const v2Alias = new lambda.Alias(this, 'UserServiceV2', {      aliasName: 'v2',      version: userServiceFn.currentVersion,    });
    // V1 API Gateway    const apiV1 = new apigateway.RestApi(this, 'UserApiV1', {      restApiName: 'User Service V1',      deployOptions: { stageName: 'prod' },    });
    const v1Users = apiV1.root.addResource('users');    const v1User = v1Users.addResource('{id}');    v1User.addMethod('GET', new apigateway.LambdaIntegration(v1Alias));
    // V2 API Gateway    const apiV2 = new apigateway.RestApi(this, 'UserApiV2', {      restApiName: 'User Service V2',      deployOptions: { stageName: 'prod' },    });
    const v2Users = apiV2.root.addResource('users');    const v2User = v2Users.addResource('{id}');    v2User.addMethod('GET', new apigateway.LambdaIntegration(v2Alias));
    // Path mapping'li custom domain    const domain = new apigateway.DomainName(this, 'CustomDomain', {      domainName: 'api.example.com',      certificate: acm.Certificate.fromCertificateArn(        this,        'Certificate',        'arn:aws:acm:us-east-1:123456789012:certificate/abc123'      ),    });
    // /v1/* V1 API'ye, /v2/* V2 API'ye map et    new apigateway.BasePathMapping(this, 'V1Mapping', {      domainName: domain,      restApi: apiV1,      basePath: 'v1',    });
    new apigateway.BasePathMapping(this, 'V2Mapping', {      domainName: domain,      restApi: apiV2,      basePath: 'v2',    });  }}

Bu, version'lar arasında complete isolation ile https://api.example.com/v1/users/123 ve https://api.example.com/v2/users/123 gibi clean URL'ler oluşturur.

CloudFront ile Header-Based Routing

Header versioning için Lambda@Edge request'leri route eder:

typescript
import { CloudFrontRequestEvent } from 'aws-lambda';
export const handler = async (event: CloudFrontRequestEvent) => {  const request = event.Records[0].cf.request;  const headers = request.headers;
  const apiVersion = headers['api-version']?.[0]?.value || '1';
  // Version'a göre uygun origin'e route et  if (apiVersion === '2') {    request.origin = {      custom: {        domainName: 'api-v2.internal.example.com',        port: 443,        protocol: 'https',        path: '',        sslProtocols: ['TLSv1.2'],        readTimeout: 30,        keepaliveTimeout: 5,        customHeaders: {}      }    };  } else {    request.origin = {      custom: {        domainName: 'api-v1.internal.example.com',        port: 443,        protocol: 'https',        path: '',        sslProtocols: ['TLSv1.2'],        readTimeout: 30,        keepaliveTimeout: 5,        customHeaders: {}      }    };  }
  return request;};

Bu yaklaşım header-based version selection'ı desteklerken URL'leri clean tutar.

GraphQL Schema Evolution

GraphQL'in felsefesi REST versioning'den farklı. Tüm API'yi version'lamak yerine, field deprecation kullanarak schema'yı continuously evolve ediyorsun:

graphql
type User {  id: ID!
  # Original field - asla remove edilmez, ama deprecated  name: String! @deprecated(reason: "Bunun yerine firstName ve lastName kullan")
  email: String!
  # Mevcut query'leri bozmadan eklenen yeni field'lar  firstName: String  lastName: String
  # Clear migration path'li deprecated field  phone: String @deprecated(reason: "Bunun yerine contactInfo.phoneNumber kullan")
  # Yeni structured contact information  contactInfo: ContactInfo}
type ContactInfo {  email: String!  phoneNumber: String  address: Address}

name ve phone query eden client'lar çalışmaya devam eder. Yeni client'lar firstName, lastName ve contactInfo query eder. GraphQL introspection API deprecation warning'lerini gösterir.

Field-level version tracking için custom directive'ler yardımcı olur:

typescript
directive @version(  added: String!  deprecated: String  removed: String) on FIELD_DEFINITION
type User {  id: ID!  name: String! @version(added: "1.0")  email: String! @version(added: "1.0")  phoneNumber: String @version(added: "2.0")
  # 3.0'da deprecated, 4.0'da removed  legacyAddress: String @version(    added: "1.0"    deprecated: "3.0"    removed: "4.0"  )
  address: Address @version(added: "3.0")}

Resolver'lar deprecated field kullanımını track edebilir:

typescript
const resolvers = {  User: {    legacyAddress: (parent, args, context) => {      const clientVersion = context.apiVersion || '1.0';
      if (semver.gte(clientVersion, '3.0')) {        context.metrics.incrementDeprecatedFieldUsage('User.legacyAddress');      }
      return parent.address?.fullAddress || '';    }  }};

Consumer-Driven Contract Testing

Contract testing consumer'lar ve provider'lar arasında version compatibility'yi ensure eder. Pact en established tool:

javascript
// Consumer test (Frontend team)// Not: Pact V2 API kullanılıyor. V3+ için `PactV3` ve farklı lifecycle method'ları kullan.const { Pact } = require('@pact-foundation/pact');
describe('User Service V2', () => {  const provider = new Pact({    consumer: 'UserWebApp',    provider: 'UserServiceV2',    port: 8080  });
  beforeAll(() => provider.setup());  afterEach(() => provider.verify());  afterAll(() => provider.finalize());
  it('id ile user alabilmeli', async () => {    await provider.addInteraction({      state: 'user mevcut',      uponReceiving: 'id 123 olan user için request',      withRequest: {        method: 'GET',        path: '/api/v2/users/123',        headers: { 'Accept': 'application/json' }      },      willRespondWith: {        status: 200,        headers: {          'Content-Type': 'application/json',          'API-Version': '2'        },        body: {          id: 123,          fullName: 'John Doe',          contactInfo: {            email: '[email protected]',            phone: '+1234567890'          }        }      }    });
    const user = await getUserById(123);    expect(user.fullName).toBe('John Doe');  });});

Backend team tüm consumer contract'ları verify eder:

javascript
const { Verifier } = require('@pact-foundation/pact');
describe('User Service Provider', () => {  it('tüm consumer contract'ları validate etmeli', async () => {    await new Verifier({      provider: 'UserServiceV2',      providerBaseUrl: 'http://localhost:3000',      pactBrokerUrl: 'https://pact-broker.example.com',      publishVerificationResult: true,      providerVersion: '2.0.0',      providerVersionTags: ['prod']    }).verifyProvider();  });});

Bu, breaking change'leri production'a ulaşmadan yakalar. Frontend fullName beklediğinde ama backend name döndürdüğünde, contract test provider verification'da fail olur.

Migration Pattern'leri

Traffic Splitting ile Parallel Run

Gradual rollout risk'i azaltır:

Feature flag'lerle implement et:

typescript
import LaunchDarkly from 'launchdarkly-node-server-sdk';
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY);
app.get('/api/users/:id', async (req, res) => {  const user = {    key: req.user?.id || 'anonymous',    email: req.user?.email,    custom: {      apiClient: req.headers['user-agent']    }  };
  const useV2 = await ldClient.variation('api-v2-rollout', user, false);
  if (useV2) {    return handleGetUserV2(req, res);  } else {    return handleGetUserV1(req, res);  }});

LaunchDarkly dashboard'u ile percentage'ı gradually artır: 10% → 25% → 50% → 75% → 100% birkaç hafta boyunca.

Shadow Mode Testing

Response'ları etkilemeden yeni version'ı test et:

typescript
app.get('/api/users/:id', async (req, res) => {  // V1'e primary request (production)  const v1Promise = handleGetUserV1(req);
  // V2'ye shadow request (testing)  const v2Promise = handleGetUserV2(req).catch(err => {    logger.error('V2 shadow request başarısız', { error: err });    return null;  });
  // V1 response'ını bekle  const v1Result = await v1Promise;
  // Result'ları asynchronously karşılaştır  v2Promise.then(v2Result => {    if (v2Result) {      compareResponses(v1Result, v2Result, req.params.id);    }  });
  // V1 response'ını döndür  res.json(v1Result);});
function compareResponses(v1: any, v2: any, userId: string): void {  const differences = deepDiff(v1, v2);
  if (differences.length > 0) {    logger.warn('V1/V2 response uyumsuzluğu', {      userId,      differences    });    metrics.increment('api.v2.response_mismatch');  }}

Bu, risk olmadan production load altında V2 behavior'ını validate eder.

Backward Compatibility için Adapter Pattern

Her iki version'ı destekleyen unified endpoint:

typescript
interface UserV1Response {  id: string;  name: string;  email: string;}
interface UserV2Response {  id: string;  fullName: string;  contactInfo: {    email: string;    phoneNumber?: string;  };}
class UserResponseAdapter {  static toV1(v2User: UserV2Response): UserV1Response {    return {      id: v2User.id,      name: v2User.fullName,      email: v2User.contactInfo.email    };  }}
app.get('/api/users/:id', async (req, res) => {  const requestedVersion = req.headers['api-version'] || '1';
  // Her zaman full V2 data fetch et  const user = await db.users.findById(req.params.id);
  if (requestedVersion === '1') {    res.setHeader('API-Version', '1');    res.setHeader('Deprecation', 'true');    return res.json(UserResponseAdapter.toV1(user));  }
  res.setHeader('API-Version', '2');  return res.json(user);});

Version Kullanımını Monitor Etmek

Hangi client'ların hangi version'ları kullandığını track et:

typescript
interface VersionMetrics {  version: string;  totalRequests: number;  uniqueClients: number;  errorRate: number;  avgLatency: number;}
// CloudWatch custom metric'lerconst cloudwatch = new AWS.CloudWatch();
function trackVersionUsage(version: string, clientId: string): void {  cloudwatch.putMetricData({    Namespace: 'API/Versioning',    MetricData: [{      MetricName: 'RequestCount',      Dimensions: [        { Name: 'Version', Value: version },        { Name: 'ClientId', Value: clientId }      ],      Value: 1,      Unit: 'Count',      Timestamp: new Date()    }]  });}

Migration progress report'ları generate et:

typescript
class MigrationTracker {  async getClientMigrationStatus(): Promise<ClientMigrationReport[]> {    const clients = await this.getAllClients();
    return clients.map(client => ({      clientId: client.id,      clientName: client.name,      currentVersion: client.apiVersion,      targetVersion: '2',      lastRequestDate: client.lastSeen,      requestCount7d: client.requests7d,      migrationStatus: this.getMigrationStatus(client)    }));  }
  private getMigrationStatus(client: Client): string {    if (client.apiVersion === '2') return 'Tamamlandı';    if (client.lastSeen < subDays(new Date(), 30)) return 'Inactive';    if (client.requests7d > 1000) return 'Yüksek Öncelik';    return 'Beklemede';  }}

Yaygın Hatalar

Yetersiz Deprecation Notice: Shutdown'dan sadece 3 ay önce sunset duyurmak client'ları zor duruma sokar. Public API'ler için minimum 12 ay daha iyi çalışır.

Minor Version'larda Breaking Change: Required field ekleyip 2.0.0 yerine 1.3.0 demek semantic versioning beklentilerini bozar. Bunu yakalamak için CI/CD'de automated OpenAPI diff kullan.

Version Proliferation: 6+ concurrent version desteklemek engineering maliyetlerini katlar. Strict sunset policy yardımcı olur: maksimum 3 version (current + previous + deprecated).

Inconsistent SDK Versioning: API version 1 ile çalışan SDK version 2.3.0 developer'ları şaşırtır. SDK major version'ını API major version'ıyla align et.

Missing Contract Test'ler: Backend değişiklikleri frontend'i bozar çünkü V1 adapter compatibility test edilmemiştir. Pact bunu önler.

Unversioned Error Response'lar: Sadece success response'ları version'larken error format'ı değişirse client error handling bozulur. Error'ları consistently version'la.

Deprecation Monitoring Yok: Major client'ların hala kullandığı V1'i bilmeden kapatmak revenue-impacting outage'lara neden olur. Sunset'tan önce usage'ı track et.

Önemli Çıkarımlar

  1. Audience'a göre versioning seç: Public API'ler için URL path, internal için header'lar, rapid iteration için GraphQL evolution.

  2. Breaking change detection'ı automate et: CI/CD'deki OpenAPI diff tool'ları accidental breaking change'leri önler.

  3. Deprecation 12+ ay gerektirir: Multi-channel communication (header'lar, email, doc'lar), usage monitoring, hard shutdown yerine gradual throttling.

  4. Concurrent version'ları sınırla: Maksimum 3 active version technical debt explosion'ını önler.

  5. Gradual rollout kullan: Feature flag'lerle 10% → 25% → 50% → 100% traffic splitting migration risk'ini azaltır.

  6. Contract testing break'leri önler: Pact, consumer'lar ve provider'lar arasındaki incompatibility'leri production'dan önce yakalar.

  7. GraphQL versioning complexity'sini elimine eder: Field-level deprecation version explosion olmadan smooth migration sağlar.

  8. Version usage'ı continuously monitor et: Deprecated version'ları kullanan client'ları track et, straggler'ları erken identify et.

Çalışan versioning stratejisi specific context'ine bağlı; public vs internal API, consumer sayısı, change frequency. Requirement'larını karşılayan en basit yaklaşımla başla, sadece gerektiğinde complexity ekle.

İlgili Yazılar