Nguyên tắc: chỉ thay đổi theo hướng cộng thêm (additive), và coi mỗi field đã phát hành là một cam kết.
Thay đổi an toàn (không cần version mới): thêm field optional vào response; thêm param optional có giá trị mặc định giữ nguyên hành vi cũ; thêm endpoint; thêm giá trị enum nếu client đã được yêu cầu bỏ qua giá trị lạ ngay từ đầu.
Thay đổi phá vỡ: xoá/đổi tên field, đổi kiểu ("total": "120000" → 120000), đổi ngữ nghĩa mà giữ nguyên tên, siết validation, đổi mã lỗi, đổi thứ tự mặc định của list, biến field optional thành bắt buộc.
Luồng đổi field an toàn — expand / migrate / contract:
1. Expand: thêm field mới song song, ghi cả hai. full_name mới nằm cạnh name cũ, đọc ghi đồng bộ.
2. Migrate: đánh dấu field cũ deprecated trong OpenAPI (deprecated: true) + changelog, đo usage theo từng field và từng client version để biết còn ai đọc.
3. Contract: chỉ gỡ field cũ khi số client còn dùng đã về ngưỡng chấp nhận được — với mobile nghĩa là chờ hết vòng đời version cũ, có thể là hàng năm. Nếu không chờ được thì gỡ ở /v2 và giữ /v1 chạy song song.
Khi tự tin về ngày tắt, dùng Deprecation + Sunset header trên response của endpoint cũ.
Kiểm soát bằng công cụ, đừng dựa vào code review:
- Contract-first: OpenAPI spec là nguồn sự thật, code sinh/validate từ spec — thay vì sinh spec từ code rồi phát hiện đã lỡ đổi.
- Chạy diff spec trong CI (oasdiff, openapi-diff...) so với spec đã phát hành; phát hiện breaking change thì fail build, muốn qua phải bump version có chủ đích.
- Contract test phía consumer để biết client thật đang phụ thuộc field nào, không chỉ dựa vào tài liệu.
- Client viết theo kiểu tolerant reader: bỏ qua field lạ, không phụ thuộc thứ tự key, có nhánh mặc định cho enum không nhận ra. Nhiều "breaking change" thực chất là do client parse quá chặt.