- Khi một web API công khai dùng đồng thời tên như Product API và đường dẫn
/api/v1, phiên bản ngữ nghĩa và cấu trúc của chính API có thể bị lệch nhau - Nếu dùng song song đường dẫn
/v1/vàmajor.minor.patch, route và hợp đồng API sẽ bị trộn lẫn, đồng thời chữ số đầu tiên của phiên bản ngữ nghĩa bị cố định trong URL - Với các thay đổi phá vỡ tính tương thích, sẽ cần đường dẫn mới và route reverse proxy, khiến thông tin về hợp đồng có thể bị phân tán giữa URL và số phiên bản
- Nếu đồng thời tạo các API kế tiếp, API hiện có trên thực tế sẽ bị khóa vào
v1, và ngay cả khi sau đó áp dụng thay đổi phá vỡ tính tương thích thì ý nghĩa của tên và đường dẫn cũng trở nên mơ hồ - Muốn biết trong việc quản lý phiên bản web API công khai, có những điểm gây vướng mắc lặp đi lặp lại nào hoặc có nguyên tắc thiết kế nào tốt hơn hay không
1 bình luận
Ý kiến trên Lobste.rs
Việc đặt
/v1/trong URL thực ra có một lợi thế lớn: nó buộc bạn không làm hỏng API đối với người dùng cho đến khi bạn tắt endpoint đóEvolving HTTP APIs và các bài viết khác của cùng tác giả đưa ra nhiều lời khuyên hữu ích
Về cơ bản, tôi gắn
/v1/,/v2/vào từng route để biểu thị các thay đổi phá vỡ tính tương thích. Nếu đây là một API công khai đang vận hành chứ không phải nỗ lực định nghĩa một tiêu chuẩn chạy trên nhiều host, thì chẳng có nhiều lý do để dùng semantic versioning đầy đủSemantic versioning tồn tại để các nhà phát triển khác có thể tự tin nâng cấp dependency mà không phải mất 20 phút đọc changelog, nhưng với API đang chạy thì mọi người không thể chọn thời điểm nhận bản minor mới hay bản vá lỗi mới
Điều được xem là thay đổi phá vỡ tương thích là khi bạn thay đổi hành vi đã được tài liệu hóa, hoặc làm hỏng các client hiện có đang phụ thuộc vào hành vi đã được tài liệu hóa đó. Có nơi còn coi cả thay đổi trong hành vi chưa được tài liệu hóa là phá vỡ, nhưng cách đó tiềm ẩn nhiều rủi ro
Ở Google họ làm như sau: AIP-185: API Versioning, AIP-180: Bacwards compatibility
Tôi thấy các tài liệu thiết kế này khá đặc thù với cách Google làm việc, nhưng khi thiết kế API tôi vẫn thường tham khảo, và một số ý tưởng trong đó thực sự rất hữu ích
Nói chung, tôi cho rằng mọi API nên cố gắng giảm tối đa các thay đổi phá vỡ tính tương thích. Ví dụ, nếu muốn đổi tên một thuộc tính thì tốt hơn nên thêm trùng thuộc tính với tên mới thay vì xóa thuộc tính cũ
Tuy vậy, cách the people at Buttondown do it cũng khá gọn gàng. Họ định nghĩa các bước migrate giữa các phiên bản API, để bên tiêu thụ có thể ghim phiên bản API của mình bằng header còn bên cung cấp vẫn tiếp tục thay đổi được
Câu trả lời kiểu “tên mới luôn được ưu tiên” nghe có vẻ hợp lý, nhưng nó có thể đổ vỡ nếu client thực hiện chu trình đọc-sửa-ghi rồi gửi lại bản sửa đổi của đối tượng do server tạo ra. Client có thể chỉ cập nhật thuộc tính cũ và gửi trả lại nguyên trạng thuộc tính mới mà nó đã bỏ qua
Có vẻ những phép biến đổi kiểu đó cũng có thể áp dụng cho việc gán hành vi, nhưng nếu tôi không bỏ sót gì thì phần đó không được đề cập
Lý tưởng nhất là đưa phiên bản vào đường dẫn và khiến các phiên bản mới có tính bổ sung. Khi đó API phiên bản cũ có thể định tuyến lại request sang phiên bản API mới hơn sau khi thực hiện các phép biến đổi đầu vào/đầu ra cần thiết
Vài năm sau, khi không còn ai dùng một phiên bản cũ nào đó nữa, bạn có thể xóa nó đi và đường dẫn
/v1/sẽ trả lỗiTrước đây tôi có đọc một chút về cách versioning API bằng content negotiation qua header
Accept. Nếu ai đã từng versioning API theo cách đó, tôi rất muốn nghe trải nghiệmTheo kinh nghiệm của tôi, versioning theo từng resource hoặc versioning toàn cục là những cách tiếp cận trực quan nhất. Với việc ngừng hỗ trợ, kết hợp dùng header phản hồi HTTP
Deprecation(RFC 9745) và cuối cùng trả về phản hồi như410 Gonecho endpoint cũ có vẻ là cách hợp lý để thúc đẩy client chuyển sang phiên bản mớiNgoài ra, tôi cũng thật sự tò mò không biết có ai từng xây dựng một API có thể tiến hóa hay chưa. Ý tôi là kiểu hệ thống nội bộ dịch request của phiên bản cũ sang request của phiên bản API mới, rồi sau khi client đã chuyển đi hoặc sau một khoảng thời gian thì thực sự xóa phiên bản cũ