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 response | Menambah field baru ke response |
| Mengubah tipe data field | Mengubah nilai default field |
| Mengubah URL structure | Menambah query parameter optional |
| Menghapus endpoint | Menambah endpoint baru |
| Mengubah required parameter menjadi tidak ada | Menambah optional parameter |
| Mengubah error response format | Menambah informasi di error response |
| Mengubah authentication method | Menambah auth method baru (kompatibel) |
2. URL Path Versioning
Strategi paling populer dan paling sederhana: nomor versi dimasukkan langsung di URL path.
# 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
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.
# 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
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
# 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
# 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
| Aspek | URL Path | Header | Query Param | Content 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 |
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
# 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
# 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"
}