Nội dung
Để thiết kế được một API tốt thật sự là một điều khó và tốn nhiều thời gian, tiền bạc, công sức, do đó chúng ta cần phải tuân theo các nguyên tắc để cho việc thiết kế, quản lí, sử dụng, nâng cấp trở nên dễ dàng hơn.
Designing an API is much like designing a transportation network. Rather than prescribing an end state or destination, a good API expands the very notion of what’s possible for developers. —Romain Huet, head of developer relations at Stripe
Để thiết kế API tốt cần tập trung vào các vấn đề như sau:
- Tuân theo các mô hình thiết kế API.
- Hiểu, đánh giá được vấn đề từ ngữ cảnh thực tế, từ đó chuyển qua ngôn ngữ lập trình để thiết kế các chức năng.
- Đóng vai trò là người dùng.
- Viết documentation, tutorial, ...
- Khắc phục các hạn chế của kiến trúc cũ.
Thiết kế API tốt nên đáp ứng các yếu tố sau:
1. Documentation
Viết documents mô tả sẽ tốn thời gian nhưng thực sự cần thiết, điều này giúp người dùng API dễ dàng tiếp cận và sử dụng, và giúp đỡ người thiết kế trong việc bảo trì và mở rộng sau này, giúp người sử dụng hiểu rõ và dùng dễ dàng.
Một số ví dụ tài liệu hay và chi tiết: Twilio, Django, and MailChimp.
2. Stability and Consistency
Ngoài việc chạy ổn định theo thời gian, các APIs cần phải nhất quán. Vì lý do nào đó mà API cần thay đổi tên tham số hoặc phương thức gửi dữ liệu,... Chúng ta nên xử lý các tham số chung trên toàn bộ API, sử dụng tính kế thừa/mô hình có sẵn để tái sử dụng các quy ước đặt tên và xử lý dữ liệu giống nhau.
Mỗi khi API có sự nâng cấp hay thay đổi version, chúng ta cần publish một số changelog về sự khác nhau giữa các API version cho người dùng, và đảm bảo các version cũ vẫn hoạt động ổn định.
http://myapisite.com/api/widgets?version=1 // or
http://myapisite.com/api/widgets/v1
3. Flexibility
APIs should provide primitives that can enable new workflows and not simply mirror the workflows of your application. The creation of an API acts as a gate for what the API’s users can do. If you provide too low-level access, you could end up with a confusing integration experience and you push too much work on the integrators.
If you provide too high-level access, you could end up with most integrations simply mirroring what your own application does. You need to find the right balance to enable workflows you hadn’t considered either as part of your application or within the API itself in order to enable innovation.
Consider what your own engineers would want in an API to build the next interesting feature for your application and then make that a part of your public API.—Kyle Daigle, director of ecosystem engineering at GitHub
Điều này đảm bảo khả năng mở rộng cũng như tuỳ biến của API, người dùng sử dụng API với nhiều mục đích và trên nhiều nền tảng khác nhau, nên linh hoạt trong cách thiết kế và các định dạng dữ liệu trả về.
Ví dụ: Để lấy dữ liệu trả về dạng JSON, chúng ta có thể sử dụng URL: /api/v1/users.json
hoặc khai báo Accept: application/json
tại HTTP header, hoặc qua chuỗi truy vấn: ?format=JSON,
..., có thể áp dụng cho các định dạng khác như: YAML, XML,...
4. Security
Security là một trong những yếu tố quan trọng nhất để xây dựng API, cần yêu cầu xác thực (authentication) và ủy quyền (authorization) mỗi khi truy cập API, một số phương pháp xác thực phổ biến như: token-based authentication, OAuth ...
Ngoài ra, chúng ta cần phải kiểm tra dữ liệu đầu vào, xác thực quyền truy cập và phân quyền dành riêng cho các resources dựa theo chức năng, etc.
Dưới đây là một số nguyên tắc thiết kế RESTful API:
1. Dùng danh từ số nhiều:
Resource | GET | POST | PUT | DELETE |
---|---|---|---|---|
- | read | create | update | |
/cars | Returns a list of cars | Create a new car | Bulk update of cars | |
/cars/711 | Returns a specific car | Method not allowed (405) | Updates a specific car | Deletes a specific car |
Không nên dùng các động từ:
GET /getAllCars
POST /createNewCar
PUT /updateAllRedCars
DELETE /deleteAllRedCars
Thay vào đó: Chỉ sử dụng danh từ số nhiều:
GET /cars
POST /cars
PUT /cars/123
DELETE /cars/123
Một số resource URIs nên thiết kế như sau:
http://api.example.com/cars
http://api.example.com/cars/{id}
http://api.example.com/users
http://api.example.com/users/{id}
http://api.example.com/users/{id}/cars
2. Phương thức GET chỉ để lấy resource
Dùng các phương thức như: PUT, POST và DELETE để cập nhật, thêm, xóa các resource, phương thức GET chỉ nên dùng để lấy thông tin mà không làm thay đổi resource.
GET /users/711?activate
or
GET /users/711/activate
3. Nhất quán trong việc đặt tên.
Sử dụng '/' để chỉ ra mối quan hệ thứ bậc.
Ký tự '/'
được sử dụng trong phần đường dẫn của URI để biểu thị mối quan hệ phân cấp giữa các resources. ví dụ:
http://api.example.com/cars
http://api.example.com/cars/{id}
http://api.example.com/users
http://api.example.com/users/{id}
http://api.example.com/users/{id}/cars
Không sử dụng '/' trong URIs.
Dấu '/' ở cuối đường dẫn URI không có ý nghĩa và có thể gây nhầm lẫn, tốt nhất nên bỏ chúng đi.
http://api.example.com/device-management/managed-devices/
http://api.example.com/device-management/managed-devices /*This is much better version*/
Sử dụng '-' để dễ đọc các URIs.
Sử dụng ký tự '-' để giúp việc đọc hiểu các đường dẫn với tên dài dễ hơn.
http://api.example.com/inventory-management/managed-entities/{id}/install-script-location //More readable
http://api.example.com/inventory-management/managedEntities/{id}/installScriptLocation //Less readable
Không dùng '_'.
Có thể sử dụng dấu '_' để ngăn cách trong api path, nhưng có thể do lỗi font/trình duyệt mà '_' sẽ không hiển thị rõ.
Vậy chúng ta nên sử dụng '-' thay vì '_'
http://api.example.com/inventory-management/managed-entities/{id}/install-script-location //More readable
http://api.example.com/inventory_management/managed_entities/{id}/install_script_location //More error prone
Sử dụng chữ thường trong URIs.
Các chữ cái thường nên được ưu tiên trong các đường dẫn URI, tham khảo thêm ở RFC 3986
http://api.example.com/GET-DEVICES // Bad
http://api.example.com/Get-Devices // Bad
http://api.example.com/get-devices // Good
Không sử dụng file extensions.
File extensions trông xấu và rối mắt, nếu không cần thiết lấy data theo một định dạng nào đó, nên bỏ để giảm độ dài của URI.
http://api.example.com/device-management/managed-devices.xml /*Do not use it*/
http://api.example.com/device-management/managed-devices /*This is correct URI*/
4. Liên kết trong resource:
Trường hợp API cần liên kết nhiều resources với nhau, vậy cần phải thiết kế liên kết để cho việc truy vấn dễ dàng hơn.
Giả sử chúng ta có 2 resources là cars và users. Để lấy tất cả cars của một user cụ thể, ta sẽ có API sau:
- GET /users/123/cars
Để xem chi tiết thông tin của một car cụ thể của user: 123, ta sẽ có 2 cách như sau:
- GET /users/123/cars/5 (Lấy thông tin của car 5 của user 123)
- GET /cars/5 (Lấy thông tin của car 5)
Tùy vào cách lấy thông tin mà dữ liệu trả về có thể khác nhau.
Một resource chỉ nên liên kết tối đa 2 đối tượng (object), việc liên kết quá nhiều đối tượng sẽ làm cho resource trở nên dễ rối và nhầm lẫn, ví dụ:
- GET
/users/1/posts/5/comments/10
Thay vào đó, chúng ta cũng có thể sử dụng bộ filter như dưới:
- GET
/users/1/posts?id=5&comments=10
5. Xây dựng bộ search/filtering
Với các API truy vấn cần lấy dữ liệu gồm nhiều conditions kết hợp thì có thể thiết bộ filter như sau:
Xây dựng sẵn bộ điều kiện truy vấn gồm các thành phần:
neq
: không bằnggt
: lớn hơngte
: lớn hơn bằnglt
: nhỏ hơnlte
: nhỏ hơn bằngin
: có trongnot_in
: không có tronglike
: khớp với ...
...
Ví dụ:
GET https://api.example.com/posts?query[field]=title&query[compare]=like&query[value]=xzy
Lệnh truy vấn sẽ như sau:
SELECT * FROM posts WHERE title like xyz%;
Hay nếu queries ở dạng phức tạp hơn chúng ta có thể chuyển sang cấu trúc như sau:
Content-Type: application/json
POST https://api.example.com/posts
{
...
filters:
[
{
"field": "user_id",
"type": "number",
"compare": "eq",
"value": 1
}, {
"field": "category_id",
"type": "number",
"compare": "in",
"value": [1,2,3,4,5]
}, {
"field": "created_at",
"type": "date",
"compare": "eq",
"value": "2019-07-01"
},...
]
...
}
Lệnh truy vấn sẽ tương tứng như sau:
SELECT * FROM posts p
JOIN post_category pct ON pct.post_id = c.id
JOIN category ct ON pct.category_id = ct.id
WHERE user_id = 1
AND category_id in(1,2,3,4,5)
AND date(created_at) = 2019-07-01;
6. Versioning:
Versioning là một điều bắt buộc với tất cả resources, việc đặt version cho resource tuân thủ 2 nguyên tắc sau:
- Bắt đầu bằng
v
và kết thúc bằng một số nguyên dương , tránh dùng số thập phân (dùngv1
thay vìv1.5
) - Versioning sẽ được đặt ở vị trí đầu tiên của resource
Ví dụ:
- GET
/v1/users/1
7. Phân trang - Paging:
Để lấy các records theo trang, chúng ta truyền các tham số như: OFFSET và LIMIT để lấy ra được những dữ liệu phù hợp.
Mặc định, để lấy danh sách dữ liệu cars, chúng ta sẽ sử dụng câu lệnh SQL dưới đây:
SELECT * FROM Cars;
Thay vì lấy toàn bộ records, chúng ta sẽ lấy số lượng nhất định, điều này cũng giúp giảm tải cho server.
GET /cars?page=1&limit=10
Câu lệnh SQL bên dưới nói rằng "trả về chỉ 10 records.
SELECT * FROM Cars LIMIT 10 OFFSET 0;
8. Tìm kiếm:
Quy tắc: attribute tên là "q”(query)
-Global search:
- GET
/search?q=fluffy+fur
-Scope search:
- GET
/users/123/cars?q=fluffy+fur
9. Sắp xếp fields:
Cho phép sắp xếp tăng dần và giảm dần theo field cụ thể.
-
GET
/cars?sort=-manufactorer,+model
+
: Sắp xếp tăng dần-
: Sắp xếp giảm dần
Câu lệnh SQL tương ứng:
Việc sắp xếp fields thường sử dụng ORDER BY, đối với số lượng record lớn, join nhiều table,... sẽ làm chậm performance đáng kể, hãy xem xét giải pháp sau đây
10. Tuỳ chọn fields trả về:
Đôi lúc chúng ta chỉ cần sử dụng một số fields cụ thể trong object, nếu như trả về toàn bộ các fields thì không cần thiết và sẽ ảnh hưởng đến performance của hệ thống (đặc biệt đối với số lượng lớn records), do đó chúng ta nên thêm bộ filter để tuỳ biến những fields trả về.
- GET /cars?fields=id,name,mode
11. Xử lí, phân loại lỗi - Error Handling:
Đối với những resources hỗ trợ nhiều định dạng dữ liệu trả về, HTTP-Header sẽ là nơi để xác định dịnh dạng đó.
- Content-Type: Khai báo request format
- Accept: Khai báo response format
Ví dụ:
- Request format
Content-type: application/json;charset=UTF-8
Content-Type: text/html; charset=UTF-8
Content-Type: multipart/form-data; boundary=something
- Response format
Accept: application/json*
Accept: text/html
Accept: image/*
// General default
Accept: */*
Nguyên tắc HTTP response
Sử dụng cặp { key:value }
như sau:
{
"id": "1",
"name": "Kenvin"
}
HTTP status code và error message
Chuẩn HTTP cung cấp hơn 70 status codes để mô tả các giá trị trả về. Dưới đây là một số status codes phổ biến hay dùng:
- 200 - OK - Eyerything is working
- 201 - Created - A new resource has been created
- 304 - Not Modified - The client can use cached data
- 400 - Bad Request - The request was invalid or cannot be served. The exact error should be explained in the error payload. E.g. "The JSON is not valid"
- 401 - Unauthorized - The request requires an user authentication
- 403 - Forbidden -The server understood the request, but is refusing it or the access is not allowed.
- 404 - Not found - There is no resource behind the URI.
- 422 - Unprocessable Entity - Should be used if the server cannot process the enitity, e.g. if an image cannot be formatted or mandatory fields are missing in the payload.
- 429 - Too Many Requests - The user has sent too many requests in a given amount of time ("rate limiting").
- 500 - Internal Server Error - API developers should avoid this error. If an error occurs in the global catch blog, the stracktrace should be logged and not returned as response.
Trong đó:
- The client application behaved erroneously (client error - 4xx response code)
- The API behaved erroneously (server error - 5xx response code)
- The client must take some additional action. (redirection - 3xx response code)
- The client and API worked (success - 2xx response code)
Lỗi có thể xảy ra xuyên suốt trong quá trình gọi API, từ lỗi về authorization đến business logic, thấp hơn là lỗi liên quan đến database.
Khi thiết kế API, chúng ta nên tổ chức và phân loại lỗi một cách có hệ thống và các dữ liệu trả về nên có ý nghĩa rõ ràng, điều này rất có ích cho việc xác định và debug sau này.
Ví dụ cụ thể cho các trường hợp như sau:
Trường hợp | Nên | Không nên |
---|---|---|
Authentication failed because token is revoked | token_revoked | invalid_auth |
Value passed for name exceeded max length | name_too_long | invalid_name |
Credit card has expired | expired_card | invalid_card |
Cannot refund because a charge has already been refunded | charge_already_refunded | cannot_refund |
Chỉ ra các danh mục lỗi xảy ra trong lúc thực hiện gọi API, nhóm các lỗi thành các danh mục và phân thành các cấp độ như sau:
Danh mục lỗi | Ví dụ |
---|---|
System-level error | Database connection issue, |
Backend service connection issue | |
Fatal error | |
Business logic error | Rate-limited |
Request fulfilled, but no results were found | |
Business-related reason to deny access to information | |
API request formatting error | Required request parameters are missing |
Combined request parameters are invalid together | |
Authorization error | OAuth credentials are invalid for request |
Token has expired |
Tiếp theo, tổ chức các errors message response có dạng:
status code, headers, error code, và error message
Error category | HTTP status | HTTP headers | Error code (machine-readable) | Error message (human-readable) |
---|---|---|---|---|
System-level error | 500 | -- | -- | -- |
Business logic error | 429 | Retry-After | rate_limit_exceeded | "You have been rate-limited. See Retry-After and try again." |
API request formatting error | 400 | -- | missing_required_parameter | "Your request was missing a {user} parameter." |
Auth error | 401 | -- | invalid_request | "Your ClientId is invalid." |
XML không phải là một lựa chọn tốt cho API response. XML khá rườm rà, gây khó khăn khi đọc & phân tích cú pháp,... Tham khảo thêm tại đây, thay vào đó chúng ta sẽ sử dụng JSON
Error response trả về có dạng JSON như sau:
{
"errors": [
{
"userMessage": "Sorry, the requested resource does not exist",
"internalMessage": "No car found in the database",
"code": 34,
"more info": "http://dev.mwaysolutions.com/blog/api/v1/errors/12345"
}
]
}
Huặc bạn cũng có thể tuỳ biến response tuỳ vào hoàn cảnh thực tế.
Tới đây, chúng ta đã có được cái nhìn tổng quát về cách tổ chức, phân loại, trả về error response trong quá trình thực hiện API, bằng cách phân loại như vậy sẽ dễ dàng hơn trong việc mở rộng, debug, xác định & xử lí lỗi.
12. Sử dụng SSL/TLS:
Điều này rất quan trọng, luôn luôn dùng SSL/TLS để mã hoá thông tin gửi đi và trả về của API.
http://api.example.com/cars // Bad
https://api.example.com/cars // Good
13. Cho phép override phương thức HTTP:
Một số proxy chỉ hỗ trợ các phương thức POST và GET. Để hỗ trợ RESTful API với những hạn chế này, API cần một cách để override phương thức HTTP.
Sử dụng custom HTTP Header X-HTTP-Method-Override để overrider lên Phương thức POST.
...
Tham khảo:
https://restfulapi.net/resource-naming/
https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design
https://www.toptal.com/api-developers/5-golden-rules-for-designing-a-great-web-api
https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api
https://hackernoon.com/restful-api-designing-guidelines-the-best-practices-60e1d954e7c9
https://swagger.io/blog/api-design/api-design-best-practices/ https://www.oreilly.com/ideas/best-design-practices-to-get-the-most-out-of-your-api