Trong phát triển web hiện đại, API Representational State Transfer (REST) đã trở thành tiêu chuẩn thực tế để xây dựng các dịch vụ web có khả năng mở rộng, dễ bảo trì và dễ hiểu. Cốt lõi của bất kỳ API RESTful nào là một khái niệm cơ bản: tài nguyên (resource). Hiểu rõ tài nguyên là gì, cách chúng được xác định và cách chúng ta tương tác với chúng là điều tối quan trọng để thiết kế và sử dụng API REST một cách hiệu quả. Bài viết này sẽ đi sâu vào bản chất của tài nguyên trong API REST, khám phá các đặc điểm, mối quan hệ của chúng với các tập hợp (collections) và các thao tác phổ biến được thực hiện trên chúng.
Ý tưởng cốt lõi đằng sau REST là một API hiển thị một tập hợp các tài nguyên và các client tương tác với các tài nguyên này bằng cách gửi yêu cầu đến các định danh duy nhất của chúng. Nhưng chính xác thì "tài nguyên" bao gồm những gì? Trong bối cảnh của API REST, tài nguyên có thể là hầu hết mọi thứ bạn có thể đặt tên. Nó có thể là một thực thể hữu hình như khách hàng, sản phẩm hoặc đơn hàng. Nó cũng có thể là một khái niệm trừu tượng như dịch vụ, giao dịch hoặc tính toán. Điều quan trọng là nó là một mục quan tâm có thể được xác định và thao tác.
Hãy nghĩ về internet như một tập hợp lớn các tài nguyên. Mỗi trang web, hình ảnh, video hoặc tài liệu bạn truy cập trực tuyến là một tài nguyên, mỗi tài nguyên có địa chỉ duy nhất của riêng nó (URL). API REST áp dụng triết lý tương tự. Cho dù đó là hồ sơ người dùng trên nền tảng mạng xã hội, một cuốn sách cụ thể trong thư viện trực tuyến hay dự báo thời tiết cho một thành phố cụ thể, mỗi thứ đều là một tài nguyên mà API cung cấp.
Bạn muốn một nền tảng Tích hợp, Tất cả trong Một để Đội ngũ Phát triển của bạn làm việc cùng nhau với năng suất tối đa?
Apidog đáp ứng mọi nhu cầu của bạn và thay thế Postman với mức giá phải chăng hơn nhiều!
Xác định Tài nguyên: Vai trò của URI
Quan trọng là, mỗi tài nguyên trong API REST phải có ít nhất một định danh duy nhất. Định danh này thường là Uniform Resource Identifier (URI). Dạng phổ biến nhất của URI được sử dụng trong các API web là Uniform Resource Locator (URL), không chỉ xác định tài nguyên mà còn cung cấp phương tiện để định vị nó.
Ví dụ, trong API để quản lý blog, một bài đăng blog cụ thể có thể được xác định bằng URI như /posts/123
. Ở đây, /posts
có thể đại diện cho một tập hợp các bài đăng và 123
là định danh duy nhất cho một bài đăng cụ thể trong tập hợp đó. Tương tự, tài nguyên người dùng có thể được xác định bằng /users/john.doe
.
Thiết kế các URI này là một khía cạnh quan trọng của thiết kế API. Các URI được thiết kế tốt thì trực quan, dễ đoán và dễ hiểu, dễ sử dụng đối với các nhà phát triển. Chúng nên hoạt động như một biển chỉ dẫn rõ ràng, cho biết bản chất của tài nguyên đang được truy cập. Thực hành tốt chỉ ra việc sử dụng danh từ để đại diện cho tài nguyên (ví dụ: /products
, /orders
) thay vì động từ (ví dụ: /getProducts
, /createOrder
). Các phương thức HTTP (GET, POST, PUT, DELETE) sau đó được sử dụng để chỉ định hành động sẽ được thực hiện trên tài nguyên được xác định bởi URI.
Tài nguyên so với Biểu diễn: Một sự phân biệt quan trọng
Điều quan trọng là phải hiểu sự khác biệt giữa tài nguyên và biểu diễn của nó. Tài nguyên là thực thể khái niệm đó - "thứ" thực tế (khách hàng, sản phẩm, ý tưởng). Mặt khác, biểu diễn là một ảnh chụp nhanh trạng thái của tài nguyên đó tại một thời điểm cụ thể, thường được định dạng theo một kiểu phương tiện cụ thể như JSON (JavaScript Object Notation) hoặc XML (eXtensible Markup Language).
Khi một client yêu cầu một tài nguyên từ API, nó không nhận được chính tài nguyên đó (một khái niệm trừu tượng nằm trên máy chủ). Thay vào đó, nó nhận được một biểu diễn của tài nguyên đó. Ví dụ, nếu bạn yêu cầu /users/jane.doe
, API có thể trả về một biểu diễn JSON như sau:JSON
{
"id": "jane.doe",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"dateJoined": "2023-01-15T10:00:00Z"
}
Đối tượng JSON này không phải là chính Jane Doe; đó là biểu diễn dữ liệu của cô ấy được lưu trữ bởi hệ thống. Cùng một tài nguyên có thể có nhiều biểu diễn khác nhau. Ví dụ, API cũng có thể cung cấp biểu diễn XML của cùng một người dùng nếu được client yêu cầu thông qua đàm phán nội dung (sử dụng các tiêu đề HTTP như Accept
).
Trạng thái của một tài nguyên có thể thay đổi theo thời gian. Nếu Jane Doe cập nhật địa chỉ email của mình, tài nguyên người dùng cơ bản sẽ thay đổi. Các yêu cầu tiếp theo cho /users/jane.doe
sẽ trả về một biểu diễn mới phản ánh trạng thái cập nhật này. Đây là lúc phần "Chuyển đổi Trạng thái" (State Transfer) của REST phát huy tác dụng: các client tương tác với tài nguyên bằng cách truy xuất và thao tác trạng thái của chúng thông qua các biểu diễn này.
Tập hợp (Collections): Tài nguyên chứa các Tài nguyên khác
Thông thường, các tài nguyên được nhóm lại với nhau thành các tập hợp. Một tập hợp bản thân nó cũng là một tài nguyên. Ví dụ, /posts
trong API blog của chúng ta là một tài nguyên tập hợp chứa các tài nguyên bài đăng riêng lẻ. Tương tự, /users
sẽ là một tập hợp các tài nguyên người dùng.
Khi một client gửi yêu cầu GET đến một URI tập hợp như /products
, API thường trả về một biểu diễn liệt kê các tài nguyên thành viên trong tập hợp đó, thường kèm theo một số thông tin tóm tắt cho mỗi tài nguyên. Danh sách này có thể trông giống như sau:JSON
[
{
"id": "prod_abc",
"name": "Laptop Pro 15",
"price": 1299.99,
"link": "/products/prod_abc"
},
{
"id": "prod_xyz",
"name": "Wireless Mouse Ergonomic",
"price": 39.99,
"link": "/products/prod_xyz"
},
// ... thêm sản phẩm
]
Lưu ý cách mỗi mục trong tập hợp thường bao gồm một liên kết (hoặc URI riêng của nó) đến tài nguyên riêng lẻ, cho phép client điều hướng đến và truy xuất đầy đủ chi tiết của một sản phẩm cụ thể nếu cần.
Thiết kế URI cho các tập hợp và các thành viên của chúng tuân theo một mô hình logic. Thông thường:
/resources
đề cập đến tập hợp (ví dụ:/orders
)./resources/{id}
đề cập đến một thành viên cụ thể trong tập hợp đó (ví dụ:/orders/567
).
Cấu trúc phân cấp này làm cho API trực quan và phù hợp với mối quan hệ khái niệm giữa tập hợp và các phần tử của nó.
Các Thao tác trên Tài nguyên và Tập hợp
Tương tác với tài nguyên và tập hợp trong API REST được thực hiện thông qua các phương thức HTTP tiêu chuẩn. Các phương thức này xác định các hành động sẽ được thực hiện. Các phương thức phổ biến nhất là:
GET: Được sử dụng để truy xuất biểu diễn của một tài nguyên hoặc một tập hợp.
GET /posts
sẽ truy xuất danh sách tất cả các bài đăng (tập hợp).GET /posts/123
sẽ truy xuất bài đăng cụ thể có ID 123. Các yêu cầu GET phải là an toàn (safe), nghĩa là chúng không được có bất kỳ tác dụng phụ nào trên máy chủ; chúng chỉ đơn thuần để truy xuất dữ liệu. Chúng cũng phải là đảm bảo tính lặp lại (idempotent), nghĩa là nhiều yêu cầu GET giống hệt nhau phải có cùng hiệu quả như một yêu cầu duy nhất (tức là trả về cùng một biểu diễn, giả sử tài nguyên không thay đổi trong thời gian đó).
POST: Chủ yếu được sử dụng để tạo một tài nguyên mới trong một tập hợp. Phần thân yêu cầu (request body) thường chứa biểu diễn của tài nguyên mới sẽ được tạo.
POST /posts
với phần thân yêu cầu chứa chi tiết của một bài đăng blog mới (ví dụ: tiêu đề, nội dung, tác giả) sẽ chỉ thị máy chủ tạo bài đăng mới đó. Máy chủ thường phản hồi với trạng thái201 Created
và thường bao gồm URI của tài nguyên mới được tạo trong tiêu đềLocation
của phản hồi. Các yêu cầu POST nói chung không an toàn (vì chúng tạo ra một tài nguyên mới) và không đảm bảo tính lặp lại (nhiều yêu cầu POST giống hệt nhau thường sẽ tạo ra nhiều tài nguyên mới). POST cũng có thể được sử dụng cho các thao tác không đảm bảo tính lặp lại khác không phù hợp với các phương thức HTTP khác, chẳng hạn như kích hoạt một quy trình hoặc gửi dữ liệu để xử lý mà kết quả không chỉ đơn giản là cập nhật một tài nguyên hiện có thể xác định.
PUT: Được sử dụng để cập nhật hoàn toàn một tài nguyên hiện có. Phần thân yêu cầu nên chứa toàn bộ biểu diễn mới của tài nguyên. Nếu tài nguyên được xác định bởi URI tồn tại, nó sẽ được thay thế bằng biểu diễn mới. Nếu nó không tồn tại, một số API có thể chọn tạo nó (mặc dù hành vi này có thể khác nhau).
PUT /posts/123
với phần thân yêu cầu chứa tiêu đề và nội dung đã cập nhật cho bài đăng 123 sẽ thay thế bài đăng 123 hiện có bằng dữ liệu mới. Các yêu cầu PUT không an toàn (vì chúng sửa đổi tài nguyên) nhưng đảm bảo tính lặp lại. Gửi cùng một yêu cầu PUT nhiều lần sẽ dẫn đến cùng một trạng thái cho tài nguyên. Ví dụ, cập nhật tiêu đề của bài đăng thành "Tiêu đề mới" nhiều lần vẫn dẫn đến tiêu đề là "Tiêu đề mới".
DELETE: Được sử dụng để xóa một tài nguyên.
DELETE /posts/123
sẽ xóa bài đăng blog có ID 123. Các yêu cầu DELETE không an toàn (chúng xóa dữ liệu) nhưng đảm bảo tính lặp lại. Xóa một tài nguyên nhiều lần sẽ có cùng kết quả như xóa nó một lần (tài nguyên đã bị xóa). Các yêu cầu DELETE tiếp theo đến cùng một URI có thể trả về404 Not Found
hoặc204 No Content
.
PATCH: Được sử dụng để cập nhật một phần tài nguyên hiện có. Không giống như PUT, yêu cầu client gửi toàn bộ biểu diễn của tài nguyên, PATCH cho phép chỉ gửi các thay đổi. Ví dụ, để chỉ cập nhật địa chỉ email của người dùng, yêu cầu PATCH chỉ cần bao gồm email mới.
PATCH /users/jane.doe
với phần thân yêu cầu như{"email": "new.email@example.com"}
sẽ chỉ cập nhật địa chỉ email của Jane, giữ nguyên các trường khác như tên của cô ấy. Các yêu cầu PATCH không an toàn. Tính đảm bảo tính lặp lại của chúng có thể gây tranh cãi và phụ thuộc vào bản chất của thao tác vá lỗi (patch). Ví dụ, thao tác PATCH nói "thêm '!' vào mô tả" không đảm bảo tính lặp lại, trong khi "đặt mô tả thành 'giá trị mới'" thì có.
Tài nguyên Đơn lẻ (Singleton Resources)
Trong khi các tập hợp và các thành viên của chúng rất phổ biến, đôi khi một tài nguyên là một thực thể độc lập, thường được gọi là tài nguyên "đơn lẻ" (singleton). Một ví dụ điển hình có thể là cấu hình của một ứng dụng cụ thể hoặc trạng thái hiện tại của một hệ thống.
Ví dụ, /application/configuration
có thể là URI cho một tài nguyên đơn lẻ đại diện cho cài đặt cấu hình của ứng dụng. Một yêu cầu GET
đến URI này sẽ truy xuất cấu hình hiện tại và một yêu cầu PUT
có thể được sử dụng để cập nhật nó. Không có "tập hợp" các cấu hình trong ngữ cảnh này; chỉ có cấu hình đó.
Tương tự, /system/status
có thể đại diện cho trạng thái hoạt động hiện tại của hệ thống.
Các Thực hành Tốt nhất để Thiết kế API dựa trên Tài nguyên
Thiết kế API lấy tài nguyên làm trung tâm bao gồm nhiều hơn việc chỉ xác định các thực thể và ánh xạ chúng tới URI. Một số thực hành tốt nhất góp phần tạo ra một API mạnh mẽ và thân thiện với người dùng:
- Sử dụng Danh từ cho URI: Như đã đề cập trước đó, URI tài nguyên nên là danh từ (ví dụ:
/products
,/users/{userId}/orders
). Động từ nên được dành riêng cho các phương thức HTTP. - Đặt tên URI nhất quán: Sử dụng quy ước đặt tên nhất quán cho các URI của bạn. Danh từ số nhiều thường được ưu tiên cho các tập hợp (ví dụ:
/customers
thay vì/customer
). Sử dụng dấu gạch nối (-
) để cải thiện khả năng đọc của các phân đoạn đường dẫn dài (ví dụ:/product-categories
) thay vì dấu gạch dưới (_
) hoặc camelCase. - Giữ URI đơn giản và phân cấp: Thiết kế URI phản ánh mối quan hệ giữa các tài nguyên. Ví dụ,
/users/{userId}/accounts/{accountId}
cho thấy rõ ràng rằng một tài khoản thuộc về một người dùng. Tuy nhiên, tránh lồng ghép quá sâu, điều này có thể làm cho URI trở nên khó sử dụng. - Không trạng thái (Statelessness): Mỗi yêu cầu từ client đến máy chủ phải chứa tất cả thông tin cần thiết để hiểu1 và xử lý yêu cầu. Máy chủ2 không nên lưu trữ bất kỳ ngữ cảnh client nào giữa các yêu cầu. Đây là nguyên tắc cốt lõi của REST và góp phần vào khả năng mở rộng.
- Tận dụng các phương thức HTTP một cách chính xác: Sử dụng GET, POST, PUT, DELETE và PATCH theo ngữ nghĩa đã định nghĩa của chúng. Không sử dụng GET để sửa đổi dữ liệu hoặc POST để truy xuất dữ liệu khi GET là phù hợp.
- Sử dụng mã trạng thái HTTP thích hợp: Trả về các mã trạng thái HTTP tiêu chuẩn để chỉ ra kết quả của yêu cầu (ví dụ:
200 OK
,201 Created
,204 No Content
,400 Bad Request
,401 Unauthorized
,403 Forbidden
,3404 Not Found
,500 Internal Server Error
). Điều này cung cấp phản hồi rõ ràng cho client. - Hỗ trợ đàm phán nội dung (Content Negotiation): Cho phép client chỉ định định dạng biểu diễn mong muốn (ví dụ: JSON, XML) bằng cách sử dụng tiêu đề
Accept
và chỉ định định dạng của phần thân yêu cầu bằng cách sử dụng tiêu đềContent-Type
. - Phiên bản hóa (Versioning): Lập kế hoạch cho sự phát triển của API bằng cách triển khai chiến lược phiên bản hóa (ví dụ:
/v1/products
). Điều này cho phép bạn giới thiệu các thay đổi gây phá vỡ mà không ảnh hưởng đến các client hiện có. - Cung cấp biểu diễn lỗi có ý nghĩa: Khi xảy ra lỗi, trả về một thông báo lỗi hữu ích trong phần thân phản hồi (thường là JSON hoặc XML) giải thích điều gì đã xảy ra.
- Hypermedia là Động cơ của Trạng thái Ứng dụng (HATEOAS): Mặc dù không phải lúc nào cũng được triển khai đầy đủ, HATEOAS là một nguyên tắc chính của REST. Điều đó có nghĩa là các biểu diễn của tài nguyên nên bao gồm các liên kết (kiểm soát hypermedia) cho phép client khám phá các hành động và tài nguyên liên quan. Ví dụ, biểu diễn của một đơn hàng có thể bao gồm các liên kết để hủy đơn hàng, xem trạng thái vận chuyển của nó hoặc xem các sản phẩm mà nó chứa. Điều này làm cho API dễ tự khám phá hơn.
Độ chi tiết của Tài nguyên
Một thách thức thiết kế phổ biến là xác định độ chi tiết phù hợp cho các tài nguyên của bạn. Một địa chỉ có nên là một tài nguyên riêng biệt hay là một phần của tài nguyên người dùng? Các mục đơn hàng có nên là tài nguyên riêng biệt hay được nhúng trong tài nguyên đơn hàng?
Không có câu trả lời đúng duy nhất; nó phụ thuộc vào các trường hợp sử dụng cụ thể và cách client thường tương tác với dữ liệu.
- Tài nguyên Riêng biệt: Nếu một thực thể (như địa chỉ) có thể được tạo, truy xuất, cập nhật hoặc xóa một cách độc lập, hoặc nếu nó được chia sẻ giữa nhiều tài nguyên khác, thì thường hợp lý khi mô hình hóa nó như một tài nguyên riêng biệt với URI riêng của nó (ví dụ:
/addresses/{addressId}
). Sau đó, bạn có thể liên kết nó từ các tài nguyên khác (ví dụ: tài nguyên người dùng có thể có trườngaddressId
hoặc liên kết đến/addresses/{addressId}
). - Tài nguyên Nhúng/Tài nguyên con: Nếu một thực thể gắn bó chặt chẽ với tài nguyên cha và không có vòng đời độc lập, thì có thể tốt hơn khi mô hình hóa nó như một phần của biểu diễn tài nguyên cha hoặc như một tài nguyên con có thể truy cập thông qua đường dẫn như
/users/{userId}/address
. Điều này có thể đơn giản hóa tương tác của client nếu thực thể con luôn được truy cập trong ngữ cảnh của tài nguyên cha.
Sự lựa chọn thường liên quan đến sự đánh đổi:
- Chattiness (Nhiều yêu cầu): Nhiều tài nguyên chi tiết có thể dẫn đến nhiều yêu cầu HTTP hơn (tăng chattiness) nếu client cần tập hợp một bức tranh hoàn chỉnh từ nhiều nguồn.
- Sao chép Dữ liệu/Phức tạp: Nhúng tài nguyên có thể dẫn đến các payload lớn hơn và khả năng sao chép dữ liệu hoặc phức tạp nếu thông tin được nhúng cũng có sẵn dưới dạng tài nguyên độc lập.
- Khả năng cache: Các tài nguyên riêng biệt thường dễ dàng cache độc lập hơn.
- Tính nguyên tử của Thao tác: Các cập nhật cho một tài nguyên đơn lẻ, có độ chi tiết thô thì vốn dĩ là nguyên tử. Quản lý tính nguyên tử trên nhiều cập nhật tài nguyên chi tiết hơn có thể phức tạp hơn.
Cân nhắc cẩn thận các yếu tố này, cùng với sự hiểu biết sâu sắc về cách API sẽ được sử dụng, là rất quan trọng để đưa ra quyết định đúng đắn về độ chi tiết của tài nguyên.
Bạn muốn một nền tảng Tích hợp, Tất cả trong Một để Đội ngũ Phát triển của bạn làm việc cùng nhau với năng suất tối đa?
Apidog đáp ứng mọi nhu cầu của bạn và thay thế Postman với mức giá phải chăng hơn nhiều!
Kết luận
Tài nguyên là các khối xây dựng nền tảng của bất kỳ API RESTful nào. Chúng đại diện cho "những thứ" mà API hiển thị và cho phép client tương tác. Bằng cách gán URI duy nhất cho tài nguyên, phân biệt giữa tài nguyên và biểu diễn của nó, và tổ chức tài nguyên thành các tập hợp logic, các nhà phát triển có thể tạo ra các API trực quan, có khả năng mở rộng và tuân thủ các nguyên tắc của REST.
Hiểu cách định nghĩa, xác định và thao tác tài nguyên bằng cách sử dụng các phương thức HTTP tiêu chuẩn là điều cần thiết cho cả nhà thiết kế và người tiêu dùng API. Kết hợp với các thực hành tốt nhất trong thiết kế URI, sử dụng mã trạng thái HTTP thích hợp và cách tiếp cận chu đáo về độ chi tiết của tài nguyên, mô hình tài nguyên được xác định rõ ràng sẽ dẫn đến các API không chỉ hoạt động tốt mà còn dễ dàng làm việc cùng. Khi bối cảnh kỹ thuật số tiếp tục phát triển, các nguyên tắc của kiến trúc hướng tài nguyên sẽ vẫn là nền tảng của giao tiếp dịch vụ web hiệu quả.