Protokol

REST API Versioning Strategies

Panduan komprehensif strategi versioning REST API — URL path, header, query parameter, content negotiation, backward compatibility, deprecation policy, dan best practices dokumentasi

1. Mengapa Versioning Penting?

Ketika API digunakan oleh banyak consumer (mobile apps, web, third-party integrations), Anda tidak bisa sembarangan mengubah response structure. Versioning memungkinkan Anda menambah fitur baru tanpa memecah aplikasi yang sudah ada.

Breaking vs Non-Breaking Changes

Breaking Change ❌Non-Breaking Change ✅
Menghapus field dari responseMenambah field baru ke response
Mengubah tipe data fieldMengubah nilai default field
Mengubah URL structureMenambah query parameter optional
Menghapus endpointMenambah endpoint baru
Mengubah required parameter menjadi tidak adaMenambah optional parameter
Mengubah error response formatMenambah informasi di error response
Mengubah authentication methodMenambah auth method baru (kompatibel)

2. URL Path Versioning

Strategi paling populer dan paling sederhana: nomor versi dimasukkan langsung di URL path.

Text
# URL Path Versioning — Contoh:
GET /api/v1/users/123
GET /api/v2/users/123

# Response v1:
{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com"
}

# Response v2 (restructure, breaking change):
{
  "data": {
    "id": 123,
    "attributes": {
      "full_name": "John Doe",
      "contact": {
        "email": "john@example.com",
        "phone": "+62812345678"
      }
    },
    "links": {
      "self": "/api/v2/users/123",
      "orders": "/api/v2/users/123/orders"
    }
  },
  "meta": {
    "api_version": "2.0",
    "request_id": "abc-123"
  }
}

# Contoh dari industri:
# Twitter:   https://api.twitter.com/2/tweets
# GitHub:    https://api.github.com/v3/users
# Stripe:    https://api.stripe.com/v1/charges
# Google:    https://www.googleapis.com/youtube/v3/videos

Implementasi di Express.js

JavaScript
const express = require('express');
const app = express();

// ===== URL Path Versioning =====

// v1 Router
const v1 = express.Router();
v1.get('/users/:id', async (req, res) => {
    const user = await db.users.findById(req.params.id);
    // v1 format: flat response
    res.json({
        id: user.id,
        name: user.name,
        email: user.email
    });
});

// v2 Router
const v2 = express.Router();
v2.get('/users/:id', async (req, res) => {
    const user = await db.users.findById(req.params.id);
    // v2 format: JSON:API style
    res.json({
        data: {
            id: user.id,
            type: 'user',
            attributes: {
                full_name: user.name,
                contact: { email: user.email }
            },
            relationships: {
                orders: { links: { related: `/api/v2/users/${user.id}/orders` } }
            }
        }
    });
});

// Mount routers
app.use('/api/v1', v1);
app.use('/api/v2', v2);

// Version negotiation: default ke latest
app.use('/api', (req, res, next) => {
    if (!req.path.match(/^\/v\d+/)) {
        // Redirect ke versi default
        return res.redirect(307, `/api/v2${req.path}`);
    }
    next();
});

// Deprecation middleware
app.use('/api/v1', (req, res, next) => {
    res.set('Deprecation', 'true');
    res.set('Sunset', 'Sat, 01 Jan 2027 00:00:00 GMT');
    res.set('Link', '</api/v2' + req.path + '>; rel="successor-version"');
    next();
});

app.listen(3000);

3. Header Versioning

Versi dinegosiasikan melalui HTTP header — URL tetap bersih dan tidak berubah antar versi.

Text
# Custom Header Versioning:
GET /api/users/123 HTTP/1.1
Host: api.example.com
X-API-Version: 2

# Atau via Accept header (content negotiation):
GET /api/users/123 HTTP/1.1
Host: api.example.com
Accept: application/vnd.myapp.v2+json

# Response headers:
HTTP/1.1 200 OK
Content-Type: application/vnd.myapp.v2+json
X-API-Version: 2
Vary: Accept, X-API-Version  ← Penting untuk caching!

# Contoh dari GitHub:
Accept: application/vnd.github.v3+json

# Contoh dari Stripe (hybrid):
# Stripe menggunakan URL versioning tapi juga header
# untuk minor version:
Stripe-Version: 2024-12-18
JavaScript
const express = require('express');
const app = express();

// ===== Header Versioning Middleware =====
function versionRouter(versionHandlers) {
    return (req, res, next) => {
        // Baca versi dari header
        let version = req.headers['x-api-version'] || '1';

        // Atau dari Accept header
        const accept = req.headers['accept'] || '';
        const match = accept.match(/application\/vnd\.myapp\.v(\d+)\+json/);
        if (match) version = match[1];

        // Set versi di request object
        req.apiVersion = parseInt(version);

        // Tambah response headers
        res.set('X-API-Version', version);
        res.set('Vary', 'Accept, X-API-Version');

        // Cari handler untuk versi ini
        const handler = versionHandlers[version] || versionHandlers['default'];
        if (!handler) {
            return res.status(400).json({
                error: `API version ${version} not supported`,
                supported_versions: Object.keys(versionHandlers)
            });
        }

        handler(req, res, next);
    };
}

// Usage
app.get('/api/users/:id', versionRouter({
    1: (req, res) => {
        const user = getUserV1(req.params.id);
        res.json(user); // flat format
    },
    2: (req, res) => {
        const user = getUserV2(req.params.id);
        res.json({ data: user, meta: { version: 2 } }); // structured format
    },
    default: (req, res) => {
        const user = getUserV2(req.params.id);
        res.json({ data: user, meta: { version: 2 } });
    }
}));

app.listen(3000);

4. Query Parameter Versioning

Text
# Query Parameter Versioning:
GET /api/users/123?version=2
GET /api/users/123?v=2
GET /api/users/123?api-version=2024-01-15

# Kelebihan:
# ✅ Mudah di-test di browser
# ✅ URL tetap relatif bersih
# ✅ Mudah di-cache (per query string)

# Kekurangan:
# ❌ Bisa ter-cache oleh CDN/proxy (masalah jika versi berubah)
# ❌ Tidak "clean" dari segi URL design
# ❌ Query parameter seharusnya untuk filtering, bukan versioning

# Digunakan oleh:
# Azure API Management: ?api-version=2024-01-01
# beberapa API enterprise

5. Content Negotiation Versioning

Text
# Content Negotiation — paling "RESTful" menurut purists

# Menggunakan Accept header dengan media type custom:
GET /api/users/123 HTTP/1.1
Accept: application/vnd.myapp.user.v2+json

# Server bisa men-negosiasikan format:
# 1. Client mengirim Accept header dengan preferred version
# 2. Server memeriksa versi yang didukung
# 3. Server merespons dengan Content-Type yang sesuai
# 4. Jika versi tidak didukung → 406 Not Acceptable

# Contoh Accept header:
# application/vnd.myapp.user.v1+json    → User format v1
# application/vnd.myapp.user.v2+json    → User format v2
# application/vnd.myapp.order.v1+json   → Order format v1
# application/vnd.myapp.v2+json         → Semua format v2

# Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.myapp.user.v2+json
Vary: Accept

# Jika versi tidak didukung:
HTTP/1.1 406 Not Acceptable
Content-Type: application/problem+json
{
  "type": "https://api.example.com/errors/unsupported-version",
  "title": "Unsupported API Version",
  "detail": "Version 3 is not supported. Available: 1, 2",
  "status": 406
}

6. Perbandingan Strategi

AspekURL PathHeaderQuery ParamContent Negotiation
Visibility⭐⭐⭐ Terlihat jelas⭐⭐ Tersembunyi⭐⭐⭐ Terlihat⭐⭐ Tersembunyi
URL Cleanliness⭐⭐ Versi di URL⭐⭐⭐ Bersih⭐⭐ Ada query⭐⭐⭐ Bersih
Caching⭐⭐⭐ Mudah⭐⭐ Perlu Vary⭐⭐ Bisa salah cache⭐⭐ Perlu Vary
Discoverability⭐⭐⭐ Intuitif⭐⭐ Perlu dokumentasi⭐⭐⭐ Mudah⭐⭐ Kompleks
Bookmark/Share⭐⭐⭐ Ya⭐ Tidak⭐⭐⭐ Ya⭐ Tidak
REST Purity⭐⭐ Bukan resource berbeda⭐⭐⭐ Negotiation⭐⭐ Kurang tepat⭐⭐⭐ Paling RESTful
Popularity⭐⭐⭐ Paling populer⭐⭐ GitHub, Stripe⭐ Azure⭐⭐ Pendekatan purist
💡 Rekomendasi

Untuk mayoritas API publik, URL path versioning (/v1, /v2) adalah pilihan terbaik karena sederhana, intuitif, mudah di-test, dan paling banyak dipahami developer. Gunakan header versioning jika Anda ingin URL bersih dan API Anda sudah stabil (Stripe, GitHub approach).

7. Backward Compatibility

Text
# Best Practices untuk Backward Compatibility:

# 1. TAMBAH, jangan hapus atau ubah
# ✅ Tambah field baru di response (consumer ignore yang tidak dikenal)
# ✅ Tambah optional parameter di request
# ❌ Jangan hapus field yang sudah ada
# ❌ Jangan ubah tipe data field

# 2. EVOLUSI tanpa breaking change
# Sebelum (v1):
{ "name": "John Doe" }

# Setelah — TETAP v1, tapi tambah field:
{ "name": "John Doe", "first_name": "John", "last_name": "Doe" }
# → Consumer lama masih bisa baca "name"
# → Consumer baru bisa pakai "first_name"/"last_name"

# 3. EXPAND contract bukan SHRINK
# ✅ Tambah enum value baru (consumer handle unknown values)
# ❌ Jangan hapus enum value

# 4. Gunakan default values
# Tambah parameter baru dengan default value yang mempertahankan
# behavior lama:
# POST /api/users
# v1: { "name": "John" }
# v2-compatible: { "name": "John", "send_welcome_email": true } ← default true (sama dengan v1)

# 5. Nullable vs Required
# ✅ Tambah field nullable (consumer cek null)
# ❌ Jangan membuat field yang dulunya optional menjadi required

8. Deprecation Policy

Text
# Deprecation Framework yang Baik:

# 1. COMMUNICATION — Beritahu consumer JAUH-JAUH hari
#    - Email blast ke semua registered developers
#    - Banner di developer portal
#    - Deprecation notice di API response headers

# 2. SUNSET HEADERS (RFC 8594)
GET /api/v1/users HTTP/1.1

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Jul 2027 00:00:00 GMT
Link: </api/v2/users>; rel="successor-version"

# 3. TIMELINE yang jelas
# ┌──────────────────────────────────────────────────────────┐
# │ Phase 1: Announcement (T-12 bulan)                       │
# │ - Email ke semua consumer                                │
# │ - Blog post, changelog                                   │
# │ - Tambah deprecation headers                              │
# ├──────────────────────────────────────────────────────────┤
# │ Phase 2: Warning Period (T-6 bulan)                      │
# │ - Response header Deprecation: true                      │
# │ - Rate limit dikurangi 50%                               │
# │ - Documentation: "Deprecated" badges                     │
# ├──────────────────────────────────────────────────────────┤
# │ Phase 3: Grace Period (T-3 bulan)                        │
# │ - Error rate tinggi → kirim email reminder               │
# │ - Rate limit dikurangi lagi                              │
# │ - Sunset header ditambahkan                              │
# ├──────────────────────────────────────────────────────────┤
# │ Phase 4: Sunset (T = 0)                                  │
# │ - Versi lama mengembalikan 410 Gone                      │
# │ - Migration guide tersedia                               │
# └──────────────────────────────────────────────────────────┘

# Error response saat sunset:
HTTP/1.1 410 Gone
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/version-sunset",
  "title": "API Version Sunset",
  "detail": "API v1 has been sunset. Please migrate to v2.",
  "status": 410,
  "migration_guide": "https://docs.example.com/migration/v1-to-v2"
}

Quiz Pemahaman

Pertanyaan 1: Strategi versioning API yang paling populer digunakan?

a) URL path versioning (/api/v1/)
b) Query parameter (?v=1)
c) Header versioning
d) Content negotiation

Pertanyaan 2: Mana yang termasuk breaking change?

a) Menambah field baru di response
b) Menambah optional query parameter
c) Menghapus field dari response
d) Menambah endpoint baru

Pertanyaan 3: HTTP status code apa yang tepat untuk endpoint yang sudah sunset?

a) 404 Not Found
b) 500 Internal Server Error
c) 410 Gone
d) 301 Moved Permanently

Pertanyaan 4: Header apa yang standar untuk menandai API deprecation (RFC 8594)?

a) X-Deprecated
b) Sunset + Deprecation
c) Warning
d) API-Status

Pertanyaan 5: Prinsip utama backward compatibility?

a) Hapus field lama, ganti yang baru
b) Tambah field baru, jangan hapus atau ubah yang lama
c) Buat versi baru setiap bulan
d) Jangan pernah mengubah response
🔍 Zoom
100%
🎨 Tema