Sớm hay muộn API anh sẽ cần thay đổi. API versioning đúng chiến lược giúp anh evolve mà không phá client cũ. Bài này phân tích 3 cách version + chiến lược deprecation thực tế từ Stripe, GitHub, Twilio.
Vì sao cần version?
API là contract giữa anh và client. Một khi public, anh không control client nâng cấp. Mobile app v1.0 vẫn còn người dùng năm sau. Partner integrate xong khó bảo họ refactor mỗi tháng.
Không version = mỗi thay đổi breaking đều có thể phá client cũ. Có version = anh đẩy v2 song song, deprecate v1 dần dần.
Cái gì là breaking change?
| Breaking (cần bump major) | Non-breaking (giữ version) |
|---|---|
| Xoá field từ response | Thêm field mới vào response |
| Đổi tên field | Thêm endpoint mới |
| Đổi type field (string → int) | Thêm query param optional |
| Đổi nghĩa enum value | Mở rộng enum (thêm value mới)* |
| Xoá endpoint | Loosen validation (cho phép input rộng hơn) |
| Thêm required field input | Optional field input |
| Đổi error code | Sửa lỗi rõ ràng (typo, perf) |
* Cảnh báo: enum mở rộng là breaking nếu client làm switch không có default branch — nó sẽ rơi vào exception. Document rõ "enum values may be added".
3 cách versioning
1. URL prefix — rõ ràng nhất
https://api.alodev.vn/v1/users
https://api.alodev.vn/v2/users
Ưu:
- Dễ debug — chỉ nhìn URL biết version
- Dễ route ở reverse proxy: nginx
location /v1/,location /v2/ - Cache HTTP rõ — mỗi version là URL riêng
- Stripe, Twilio, Square dùng cách này
Nhược:
- URL không "canonical" — same resource có 2+ URL
- Refactor lớn khi bump major — phải copy code gần như toàn bộ
2. Header — sạch URL
GET /users HTTP/1.1
Accept: application/vnd.alodev.v2+json
Hoặc custom header:
GET /users HTTP/1.1
X-API-Version: 2
Ưu: URL canonical không đổi. Nhược: khó test bằng browser/curl, khó cache CDN, khó debug. GitHub đã dùng cách này (Accept: application/vnd.github.v3+json) nhưng đa số dev vẫn confused.
3. Query param — không khuyến cáo
GET /users?v=2
Tệ nhất. Lẫn với filter, dễ quên, không clean. Tránh.
Khi nào bump version?
Quy tắc Stripe: chỉ bump khi buộc phải. 90% thay đổi là non-breaking. Bump major là cuộc tốn:
- Maintain song song nhiều version (DB schema, business logic)
- Documentation viết lại
- Migration guide cho client
- Email + announcement
Trước khi bump, hỏi: có cách nào làm non-breaking không?
// Thay vì đổi field user.name thành full_name (breaking)
// Trả cả 2:
{
"id": 1,
"name": "An Nguyen", // legacy
"full_name": "An Nguyen", // new field
// ... và document deprecate "name" trong 12 tháng
}
Deprecation timeline
Khi đã quyết bump v2, plan deprecation v1:
Tháng 0: Launch v2. v1 vẫn hoạt động bình thường.
Tháng 0: Email + blog post công bố v2 + migration guide.
Tháng 0+: API v1 trả header Sunset + Deprecation:
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.alodev.vn/migrate-v2>; rel="deprecation"
Tháng 3: Email reminder cho client còn dùng v1 (theo API key).
Tháng 6: Status page banner.
Tháng 9: Email final warning. Statuspage incident.
Tháng 12: Tắt v1, trả 410 Gone với link migration guide.
Header Sunset là chuẩn RFC 8594. Client library tốt sẽ tự log warning.
Implement Express với 2 version
// Tách router theo version
import { Router } from 'express'
const v1 = Router()
v1.get('/users', listUsersV1) // schema cũ
v1.post('/users', createUserV1)
const v2 = Router()
v2.get('/users', listUsersV2) // schema mới
v2.post('/users', createUserV2)
// Middleware add deprecation header cho v1
v1.use((req, res, next) => {
res.setHeader('Deprecation', 'true')
res.setHeader('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT')
res.setHeader('Link', '<https://docs.alodev.vn/migrate-v2>; rel="deprecation"')
next()
})
app.use('/v1', v1)
app.use('/v2', v2)
Code có thể share — controller v2 có thể call lại logic v1 nếu chỉ thêm field. Đừng duplicate business logic — chỉ duplicate adapter (request/response shape).
GraphQL evolve schema
GraphQL không version cả endpoint — schema evolve field-level:
type User {
id: ID!
name: String! @deprecated(reason: "Use fullName instead")
fullName: String!
email: String!
}
Client cũ vẫn query name — vẫn hoạt động. Client mới query fullName. GraphiQL hiện cảnh báo deprecation. Sau 12 tháng, anh có thể xoá name nếu không còn ai query (kiểm tra qua usage log).
Đây là một trong những lý do team chọn GraphQL vs REST — evolve schema mềm hơn nhiều.
Checklist API versioning đúng chuẩn
- ✅ Mọi API mới bắt đầu với /v1/ prefix
- ✅ Document rõ contract: OpenAPI spec cho v1, v2
- ✅ Bump major chỉ khi không thể non-breaking
- ✅ Header Deprecation + Sunset khi version cũ tắt sắp tắt
- ✅ Migration guide có ví dụ trước/sau
- ✅ Usage log theo version để biết khi nào tắt được v1 an toàn
- ✅ Email + status page communication 6-12 tháng
Kết luận
API versioning chiến lược không phức tạp — chỉ cần kỷ luật. Bắt đầu /v1/, evolve non-breaking khi có thể, bump v2 với deprecation timeline rõ. Anh sẽ cảm ơn bản thân khi 2 năm sau cần thay đổi mà không phá hệ thống của khách hàng. Kết hợp với REST API design chuẩn để có nền tảng vững từ đầu.