- Trong kỹ thuật phần mềm, API là công cụ cốt lõi, và một API tốt nên quen thuộc và đơn giản đến mức nhàm chán
- Một khi API đã được công khai thì rất khó thay đổi, vì vậy nguyên tắc không phá vỡ môi trường của người dùng (WE DO NOT BREAK USERSPACE) là rất quan trọng
- Khi buộc phải thay đổi, cần có quản lý phiên bản (versioning), nhưng đây là một cái ác cần thiết làm tăng mạnh độ phức tạp và chi phí bảo trì
- Chất lượng API rốt cuộc phụ thuộc vào giá trị của chính sản phẩm, và một sản phẩm được thiết kế tệ sẽ khiến việc tạo ra API tốt trở nên khó khăn
- Để đảm bảo tính ổn định và khả năng mở rộng, cần cân nhắc xác thực bằng API key, tính idempotency, rate limit, phân trang dựa trên con trỏ
Mở đầu: Tầm quan trọng và bối cảnh của thiết kế API
- Một trong những công việc chính của kỹ sư phần mềm hiện đại là tương tác với API
- Tác giả cũng có kinh nghiệm thiết kế/triển khai/sử dụng nhiều dạng API công khai và nội bộ khác nhau như REST, GraphQL, công cụ dòng lệnh
- Những lời khuyên hiện có về thiết kế API thường có xu hướng bám chặt vào các khái niệm phức tạp (định nghĩa REST, HATEOAS, v.v.)
- Bài viết này tổng hợp các nguyên tắc thiết kế API thực dụng dựa trên kinh nghiệm thực tế
Cân bằng giữa sự quen thuộc và tính linh hoạt: Điều kiện đầu tiên của một API tốt
- Một API tốt là API 'bình thường và nhàm chán', tức cách dùng nên tương tự các API mà người dùng từng gặp trước đây
- Người dùng tập trung vào việc đạt được mục tiêu của họ hơn là vào chính API, nên cần thiết kế có rào cản gia nhập thấp
- Một khi API đã được công khai thì rất khó thay đổi, nên cần thận trọng ngay từ giai đoạn thiết kế ban đầu
- Lập trình viên luôn muốn API càng đơn giản càng tốt, nhưng đồng thời cũng phải để lại không gian cho tính linh hoạt dài hạn
- Kết quả là, cân bằng giữa sự quen thuộc và tính linh hoạt lâu dài là bài toán cốt lõi
Tuyệt đối không phá vỡ không gian người dùng (WE DO NOT BREAK USERSPACE)
- Việc thêm trường vào cấu trúc phản hồi hiện có trong đa số trường hợp là không vấn đề gì
- Nhưng xóa trường, đổi kiểu hoặc đổi cấu trúc sẽ dẫn đến việc làm hỏng toàn bộ mã phía consumer
- Người duy trì API có trách nhiệm không cố ý làm hỏng phần mềm của người dùng hiện tại
- Ngay cả lỗi chính tả trong header HTTP
referer cũng không bị sửa là vì văn hóa bảo toàn không gian người dùng
Thay đổi API mà không làm gãy: Chiến lược quản lý phiên bản
- Chỉ khi thực sự cần thiết mới cho phép thay đổi phá vỡ tương thích trong API, và lúc đó quản lý phiên bản là câu trả lời
- Cần vận hành song song phiên bản cũ và mới, đồng thời thúc đẩy quá trình chuyển đổi dần dần
- Định danh phiên bản có thể được thể hiện qua nhiều cách như URL (
/v1/), header, v.v., và người dùng có thể chuyển đổi theo tốc độ của riêng họ
- Quản lý phiên bản có nhược điểm là chi phí bảo trì khổng lồ (tăng endpoint, kiểm thử, hỗ trợ) và gây nhầm lẫn cho người dùng
- Ngay cả khi có một tầng dịch nội bộ như Stripe, vẫn không thể tránh được độ phức tạp gốc rễ
- Chỉ nên đưa quản lý phiên bản API vào như lựa chọn cuối cùng
Yếu tố quyết định thành công của API hoàn toàn phụ thuộc vào giá trị sản phẩm
- Về bản chất, API chỉ là giao diện của một sản phẩm kinh doanh thực tế
- Những API như OpenAI, Twilio rốt cuộc cũng được dùng vì thứ người dùng muốn là chính chức năng mà API cung cấp
- Nếu sản phẩm có giá trị thì người ta vẫn sẽ dùng dù API bất tiện
- Chất lượng API là một thuộc tính kiểu biên lợi thế: chỉ trở thành yếu tố lựa chọn khi năng lực cạnh tranh cốt lõi tương đương nhau
- Ngược lại, một sản phẩm không có API sẽ là rào cản lớn với người dùng kỹ thuật
Nếu thiết kế sản phẩm tệ thì API cũng không thể tốt
- Dù có API hoàn thiện về mặt kỹ thuật, nếu sản phẩm không có sức hút thị trường thì cũng không có nhiều ý nghĩa
- Quan trọng hơn, nếu cấu trúc tài nguyên cơ bản là phi logic hoặc kém hiệu quả thì điều đó cũng sẽ lộ rõ trong API
- Ví dụ, một hệ thống lưu bình luận dưới dạng linked list sẽ khiến ngay cả thiết kế RESTful cũng khó trở nên tự nhiên
- Những vấn đề kỹ thuật có thể bị che đi trong UI sẽ bị phơi bày hết trong API, đồng thời ép người dùng phải hiểu hệ thống ở mức không cần thiết
Xác thực (Authenticaton) và sự đa dạng của người dùng
- Cần обязательно hỗ trợ xác thực bằng API key có vòng đời dài
- Dù có thể hỗ trợ thêm các cách an toàn hơn như OAuth, rào cản gia nhập của API key vẫn thấp hơn hẳn
- Người dùng API không chỉ có kỹ sư mà còn có cả người không phải lập trình viên (bán hàng, lập kế hoạch, sinh viên, lập trình viên hobby, v.v.)
- Những yêu cầu xác thực khó hoặc phức tạp (như OAuth) sẽ trở thành rào cản với người dùng không chuyên
Tính idempotency và xử lý retry
- Với các request mang tính hành động (ví dụ: thanh toán, thay đổi trạng thái, v.v.), tính an toàn khi retry nếu thất bại là rất quan trọng
- Idempotency là việc đảm bảo dù gửi cùng một request nhiều lần thì kết quả cũng chỉ được xử lý một lần
- Cách làm tiêu chuẩn là truyền idempotency key qua tham số hoặc header để ngăn xử lý trùng lặp
- Việc lưu idempotency key chỉ cần một kho key/value đơn giản như Redis, và trong đa số trường hợp có thể áp dụng thời hạn hết hạn định kỳ
- Thường thì không cần cho các request đọc/xóa (theo kiểu REST)
An toàn API và giới hạn tốc độ (Rate limiting)
- Request API qua code có thể diễn ra nhanh hơn thao tác của người dùng rất nhiều
- Chỉ một API vô tình được triển khai cũng có thể bị sử dụng theo những cách ngoài dự kiến (ví dụ: hệ thống chat quy mô lớn)
- Giới hạn tốc độ (ratelimit) là bắt buộc, và cần áp dụng nghiêm ngặt hơn với các tác vụ tốn kém
- Cũng nên cân nhắc tùy chọn tạm vô hiệu hóa API cho khách hàng cụ thể (
killswitch)
- Cần cung cấp thông tin về giới hạn tốc độ qua header phản hồi như
X-Limit-Remaining, Retry-After, v.v.
Chiến lược phân trang (Pagination)
- Để trả về hiệu quả các tập dữ liệu lớn (ví dụ: hàng triệu ticket), phân trang là bắt buộc
- Phân trang dựa trên offset đơn giản nhưng sẽ chậm dần với dữ liệu lớn
- Phân trang dựa trên con trỏ (cursor-based) vẫn hiệu quả với tập dữ liệu rất lớn mà không làm giảm hiệu năng truy vấn
- Cách dựa trên con trỏ có phần khó triển khai và sử dụng hơn, nhưng về dài hạn rất có thể là thay đổi thiết yếu
- Khôn ngoan là đưa
next_page hoặc trường tương tự vào phản hồi để chỉ rõ con trỏ cho request tiếp theo
Trường tùy chọn và quan điểm về GraphQL
- Những trường tốn kém hoặc chậm nên bị loại khỏi phản hồi mặc định và chỉ được thêm khi cần
- Có thể cho phép bao gồm dữ liệu liên quan bằng tham số như
includes
- GraphQL có ưu điểm về độ linh hoạt cấu trúc dữ liệu, nhưng cũng có các vấn đề như khó tiếp cận hơn với người không phải lập trình viên, làm phức tạp caching/các edge case, tăng độ khó triển khai phía sau
- Theo kinh nghiệm thực tế, chỉ nên áp dụng GraphQL khi thực sự cần thiết
Đặc điểm của API nội bộ
- API nội bộ trong công ty khác với API bên ngoài (công khai) ở nhiều điều kiện
- Người dùng chủ yếu là kỹ sư phần mềm chuyên nghiệp, nên có thể chấp nhận xác thực phức tạp hơn hoặc thay đổi phá vỡ tương thích
- Dù vậy, các nguyên tắc thiết kế để đảm bảo idempotency, ngăn ngừa sự cố và giảm gánh nặng vận hành vẫn còn nguyên giá trị
Tóm tắt
- API khó thay đổi nhưng phải dễ sử dụng
- Không phá vỡ không gian người dùng là nghĩa vụ quan trọng nhất của người duy trì API
- Quản lý phiên bản API có chi phí lớn nên chỉ dùng như biện pháp cuối cùng
- Cuối cùng, chất lượng API bị chi phối bởi giá trị cốt lõi của sản phẩm
- Một sản phẩm được thiết kế tệ thì dù vá víu ở tầng API cũng có giới hạn lớn
- Quan trọng là hỗ trợ cách xác thực đơn giản, bắt buộc có idempotency cho các request hành động thiết yếu, cùng các biện pháp ổn định như giới hạn tốc độ/phân trang
- API nội bộ có chiến lược khác tùy theo mục đích và đối tượng, nhưng vẫn đòi hỏi sự thận trọng trong thiết kế
- REST, JSON hay các định dạng như OpenAPI không phải là điểm tranh luận cốt lõi. Tài liệu hóa rõ ràng mới quan trọng hơn
Chưa có bình luận nào.