Mở đầu — vì sao bài này quan trọng
Nếu bạn đã đi qua các bài trước về SQL, schema và index, thì bạn đang nắm rất chắc "phần ruột" của hệ thống — nơi dữ liệu nằm yên. Nhưng dữ liệu chỉ có giá trị khi nó được trao đổi giữa các hệ thống. REST API chính là "cánh cửa" để các ứng dụng nói chuyện với nhau: app mobile gọi backend, hệ thống ngân hàng gọi cổng thanh toán, sàn thương mại điện tử gọi đối tác giao hàng. Với một Technical BA, REST API không phải thứ "để dev lo". Bạn chính là người viết spec mô tả cánh cửa đó trông như thế nào, mở ra sao, ai được phép vào, và khi có lỗi thì hệ thống phản hồi gì.
Tôi đã chứng kiến rất nhiều dự án ở Việt Nam trễ tiến độ chỉ vì spec API mơ hồ. Một bên hiểu endpoint trả về danh sách, một bên nghĩ trả về một phần tử. Một bên tưởng status=1 là "đã thanh toán", bên kia hiểu là "đang chờ". Những hiểu lầm này không phải lỗi code — chúng là lỗi thiết kế và mô tả. Khi bạn nắm vững các nguyên tắc REST, bạn sẽ viết được spec mà cả dev hai phía đọc xong là hiểu giống nhau, và đó là giá trị cốt lõi của một Technical BA.
Bài này tập trung vào các nguyên tắc thiết kế REST — tư duy nền tảng. Việc viết spec chi tiết bằng OpenAPI/Swagger sẽ nằm ở bài sau, còn versioning, authentication, rate limiting là những bài riêng. Ở đây, ta học cách "nghĩ theo kiểu REST".
Khái niệm cốt lõi
REST (Representational State Transfer) không phải một công nghệ, mà là một phong cách kiến trúc do Roy Fielding đề xuất năm 2000. Hiểu đơn giản: thay vì coi hệ thống là tập hợp các "hành động" (như RPC kiểu cũ), REST coi mọi thứ là resource (tài nguyên) và bạn tác động lên chúng bằng một số ít động từ chuẩn của HTTP.
Resource và URI — danh từ, không phải động từ
Nguyên tắc đầu tiên và quan trọng nhất: URL trỏ tới tài nguyên (danh từ), còn hành động được thể hiện qua HTTP method (động từ). Đây là chỗ rất nhiều spec sai.
Sai (kiểu RPC): /getOrder?id=123, /createUser, /deleteProduct?id=5
Đúng (kiểu REST):
GET /orders/123— lấy đơn hàng 123POST /users— tạo người dùng mớiDELETE /products/5— xoá sản phẩm 5
/orders, /customers), và phân cấp thể hiện quan hệ: /customers/42/orders nghĩa là "các đơn hàng của khách hàng 42".Sáu nguyên tắc (constraints) của REST
REST có sáu ràng buộc kiến trúc. Một API gọi là "RESTful" khi tuân thủ chúng:
- Client-Server — Tách biệt giao diện (client) và lưu trữ dữ liệu (server). Client lo việc hiển thị, server lo việc dữ liệu và logic. Nhờ tách biệt, đội mobile và đội backend có thể phát triển song song miễn là thống nhất "hợp đồng" API.
- Stateless (không trạng thái) — Mỗi request phải chứa đầy đủ thông tin để server xử lý; server không lưu session của client giữa các request. Nếu client cần xác thực, mỗi request phải mang theo token. Lợi ích: bạn có thể đặt nhiều server sau load balancer, request nào rơi vào server nào cũng xử lý được — cực kỳ quan trọng khi scale.
- Cacheable (có thể cache) — Response phải nói rõ nó cache được hay không (qua header như
Cache-Control,ETag). Cache giảm tải server và tăng tốc cho người dùng.
- Uniform Interface (giao diện đồng nhất) — Mọi tài nguyên được truy cập theo cùng một quy ước: cùng cách đặt URL, cùng bộ HTTP method, cùng cách trả lỗi. Đây là thứ làm API "dễ đoán".
- Layered System (hệ thống phân tầng) — Client không cần biết nó đang nói chuyện trực tiếp với server hay qua một tầng trung gian (gateway, cache, load balancer). Điều này cho phép chèn các tầng bảo mật, cache mà không đổi client.
- Code on Demand (tùy chọn) — Server có thể gửi code thực thi cho client (hiếm dùng, nên thường bỏ qua).
HTTP Methods — bộ động từ chuẩn
| Method | Ý nghĩa | Idempotent? | An toàn (safe)? |
|---|---|---|---|
| GET | Đọc tài nguyên | Có | Có |
| POST | Tạo mới | Không | Không |
| PUT | Thay thế toàn bộ | Có | Không |
| PATCH | Cập nhật một phần | Không bắt buộc | Không |
| DELETE | Xoá | Có | Không |
DELETE /orders/5 gọi 3 lần thì đơn vẫn bị xoá, không gây lỗi). "Safe" nghĩa là không thay đổi dữ liệu. GET là method an toàn — đừng bao giờ để một GET làm thay đổi dữ liệu (lỗi kinh điển: dùng GET /deleteUser?id=1).HTTP Status Code — ngôn ngữ chung của lỗi
Status code nói cho client biết chuyện gì đã xảy ra. BA phải quy ước rõ trong spec:
- 2xx — Thành công:
200 OK(thành công chung),201 Created(tạo mới thành công),204 No Content(thành công nhưng không có body, hay dùng cho DELETE). - 3xx — Chuyển hướng:
304 Not Modified(dùng với cache). - 4xx — Lỗi do client:
400 Bad Request(dữ liệu gửi sai),401 Unauthorized(chưa đăng nhập),403 Forbidden(đã đăng nhập nhưng không có quyền),404 Not Found(không tìm thấy tài nguyên),409 Conflict(xung đột, ví dụ tạo trùng),422 Unprocessable Entity(dữ liệu đúng định dạng nhưng sai nghiệp vụ). - 5xx — Lỗi do server:
500 Internal Server Error,503 Service Unavailable.
Tình huống thực tế
Ví dụ 1 — Sàn TMĐT "ChợViệt": URL kiểu RPC khiến tài liệu nổ tung
Một sàn thương mại điện tử giả định tên ChợViệt có khoảng 40 endpoint cho phần đơn hàng, được thiết kế từ thời đầu theo kiểu RPC: /getOrderList, /getOrderDetail, /updateOrderStatus, /cancelOrder, /getOrderHistory... Mỗi khi cần thêm tính năng, dev lại đẻ ra một endpoint mới với động từ mới. Sau hai năm, danh sách endpoint phình lên hơn 180 cái, tài liệu dài 90 trang, và lập trình viên mới mất cả tuần mới hiểu được.
Khi đối tác là một công ty logistics muốn tích hợp, họ liên tục hỏi lại vì không đoán được tên endpoint. BA dự án ngồi lại tái thiết kế theo REST: gom toàn bộ về tài nguyên /orders. Lấy danh sách là GET /orders, chi tiết là GET /orders/{id}, huỷ đơn là POST /orders/{id}/cancellation hoặc cập nhật trạng thái qua PATCH /orders/{id}. Số endpoint giảm còn khoảng 25.
Bài học: khi tài nguyên rõ ràng và method đúng chuẩn, API tự "thu gọn" lại. Đối tác mới chỉ cần biết quy ước là đoán được phần lớn endpoint mà không cần tra cứu. Tài liệu ngắn đi không phải vì bớt tính năng, mà vì thiết kế nhất quán.
Ví dụ 2 — Ngân hàng số: bài học đắt giá về stateless và idempotency
Một ngân hàng số tại Đông Nam Á triển khai API chuyển khoản. Phiên bản đầu, server lưu trạng thái giao dịch trong session trên một máy chủ cụ thể. Khi lưu lượng tăng, họ thêm máy chủ thứ hai sau load balancer. Lập tức phát sinh lỗi: request bước 1 (khởi tạo) rơi vào máy A, bước 2 (xác nhận) rơi vào máy B — máy B không có session nên báo "phiên không hợp lệ". Hàng nghìn giao dịch lỗi trong giờ cao điểm.
Đội kỹ thuật phải tái thiết kế theo nguyên tắc stateless: mỗi request mang theo đầy đủ token và mã giao dịch, không phụ thuộc máy chủ nào. Đồng thời, BA bổ sung yêu cầu idempotency cho POST /transfers: client gửi kèm một Idempotency-Key duy nhất. Nếu mạng chập chờn khiến client gửi lại cùng request, server nhận ra key đã xử lý và trả về kết quả cũ thay vì trừ tiền hai lần.
Bài học: stateless không chỉ là lý thuyết — nó là điều kiện để hệ thống scale ngang. Và với những thao tác nhạy cảm như chuyển tiền, BA phải chủ động đưa idempotency vào spec ngay từ đầu, chứ không đợi sự cố. (Idempotency có hẳn một bài riêng vì độ quan trọng của nó, nhưng ngay từ thiết kế REST bạn đã cần nghĩ tới.)
Ví dụ 3 — Startup giao đồ ăn: status code dùng sai khiến app "đứng hình"
Một startup giao đồ ăn ở TP.HCM thiết kế API mà mọi response đều trả về 200 OK, kể cả khi lỗi — chỉ phân biệt qua một trường error_code trong body. App mobile vì thế phải tự parse body để biết thành công hay thất bại. Khi backend gặp sự cố và trả về trang lỗi HTML thay vì JSON, app không parse được, hiểu nhầm là thành công, và hiển thị "Đặt món thành công" dù đơn chưa hề được tạo. Khách chờ mãi không thấy shipper.
BA rà soát và chuẩn hoá: lỗi xác thực trả 401, không đủ quyền trả 403, món hết hàng trả 409 Conflict, lỗi hệ thống trả 5xx. App mobile chỉ cần dựa vào status code để quyết định luồng, body chỉ để hiển thị thông điệp chi tiết.
Bài học: status code là hợp đồng giữa client và server. Khi dùng đúng, client xử lý lỗi gọn gàng và đáng tin. Khi "nhét tất cả vào 200", bạn phá vỡ uniform interface và biến mọi lỗi thành quả bom hẹn giờ.
Hướng dẫn từng bước
Khi cần thiết kế (hoặc review) một REST API ở góc độ BA, hãy đi theo trình tự sau:
- Liệt kê tài nguyên (resource). Đọc yêu cầu nghiệp vụ và gạch chân các danh từ chính: khách hàng, đơn hàng, sản phẩm, thanh toán. Mỗi danh từ thường là một resource với URL số nhiều:
/customers,/orders,/products,/payments.
- Xác định quan hệ và phân cấp. Đơn hàng thuộc về khách hàng? Thiết kế
/customers/{id}/orders. Nhưng đừng lồng quá sâu — quá ba tầng là dấu hiệu thiết kế có vấn đề./customers/42/orders/100/items/3đã là giới hạn nên dừng.
- Gán HTTP method cho từng thao tác. Tạo dùng POST, đọc dùng GET, thay thế dùng PUT, sửa một phần dùng PATCH, xoá dùng DELETE. Tự hỏi: thao tác này có idempotent không? Nếu là tạo mới có rủi ro trùng (thanh toán, đơn hàng), ghi chú yêu cầu idempotency-key.
- Quy ước status code cho từng kịch bản. Với mỗi endpoint, viết rõ: thành công trả mã gì, lỗi nghiệp vụ trả mã gì, lỗi không tìm thấy trả gì. Lập một bảng. Đây là phần dev hay bỏ sót nhất.
- Thiết kế cấu trúc request/response. Thống nhất định dạng JSON, quy ước đặt tên trường (chọn
snake_casehoặccamelCaserồi giữ nhất quán toàn hệ thống), định dạng ngày giờ (khuyến nghị ISO 8601 kèm múi giờ), và định dạng lỗi chuẩn (ví dụ luôn cóerror_codevàmessage).
- Xử lý truy vấn danh sách. Endpoint trả danh sách phải hỗ trợ phân trang (
?page=2&limit=20), lọc (?status=paid), sắp xếp (?sort=-created_at). Đừng đểGET /orderstrả về 2 triệu bản ghi một lần.
- Rà soát tính nhất quán. Đọc lại toàn bộ: các endpoint có cùng quy ước đặt tên không? Cùng cấu trúc lỗi không? Một dev mới đọc có đoán được endpoint tiếp theo không? Đó là phép thử "uniform interface".
Lỗi thường gặp & mẹo
Dùng động từ trong URL. /createOrder, /getList... Đây là lỗi phổ biến nhất. Mẹo: nếu trong URL có động từ, gần như chắc chắn bạn đang thiết kế sai — hãy chuyển động từ đó thành HTTP method.
Để GET thay đổi dữ liệu. GET /orders/5/cancel nghe có vẻ tiện nhưng cực kỳ nguy hiểm: trình duyệt, crawler, hay cơ chế prefetch có thể vô tình gọi nó và huỷ đơn của khách. GET phải luôn "an toàn".
Lạm dụng status code 200. Trả 200 cho cả lỗi khiến client không thể xử lý lỗi một cách hệ thống. Hãy để status code phản ánh đúng kết quả.
Lồng tài nguyên quá sâu. URL dài 5-6 tầng vừa khó đọc vừa khó bảo trì. Mẹo: khi đã có ID duy nhất, truy cập thẳng /items/{id} thay vì lồng qua nhiều cha.
Quên phân trang. Một endpoint danh sách không phân trang sẽ sập khi dữ liệu lớn. Luôn mặc định có limit, và đặt giá trị tối đa (ví dụ limit không vượt quá 100).
Không thống nhất định dạng lỗi. Mỗi endpoint trả lỗi một kiểu khiến client phải viết hàng chục cách xử lý. Mẹo: định nghĩa một "lỗi chuẩn" dùng chung toàn hệ thống và ghi vào tài liệu nền tảng.
Mẹo cho BA: khi viết spec, hãy luôn kèm ví dụ request và response thật (mẫu JSON cụ thể với dữ liệu giả). Một ví dụ cụ thể có giá trị hơn mười dòng mô tả trừu tượng — dev đọc xong là code được ngay.
Bài tập thực hành
Hãy lấy một nghiệp vụ quen thuộc: quản lý khoá học trên một nền tảng học trực tuyến (học viên, khoá học, bài học, đăng ký học, đánh giá).
- Liệt kê các tài nguyên chính và viết URL số nhiều cho từng cái.
- Thiết kế các endpoint cho những thao tác sau, ghi rõ HTTP method và URL:
- Với endpoint "đăng ký khoá học", lập bảng status code: thành công trả gì, khi học viên đã đăng ký rồi trả gì, khi khoá học không tồn tại trả gì, khi học viên chưa đăng nhập trả gì.
- Viết một ví dụ JSON cho response của
GET /courses/{id}, gồm các trường hợp lý (id, tên, lĩnh vực, số bài học, ngày tạo theo ISO 8601).
- Tự kiểm tra: thiết kế của bạn có endpoint nào chứa động từ trong URL không? Có thao tác tạo mới nào cần idempotency không? Tất cả lỗi có dùng chung một cấu trúc không?
Tóm tắt
REST là một phong cách kiến trúc coi mọi thứ là tài nguyên (danh từ) và tác động lên chúng qua bộ động từ HTTP chuẩn. Sáu nguyên tắc nền tảng — client-server, stateless, cacheable, uniform interface, layered system và code-on-demand — định hình cách một API "tốt" trông như thế nào, trong đó stateless và uniform interface là hai thứ ảnh hưởng trực tiếp nhất tới khả năng mở rộng và dễ dùng.
Với vai trò Technical BA, ba việc quan trọng nhất khi thiết kế hay review REST API là: dùng danh từ cho URL và động từ cho method, quy ước status code rõ ràng cho mọi kịch bản, và giữ tính nhất quán xuyên suốt để API trở nên dễ đoán. Những nguyên tắc này không phải lý thuyết khô khan — như ba tình huống ở trên cho thấy, làm sai chúng dẫn tới tài liệu phình to, giao dịch lỗi khi scale, và app hiểu nhầm trạng thái. Nắm chắc tư duy REST ở bài này chính là nền móng để bạn viết spec OpenAPI chuẩn, xử lý versioning và bảo mật ở các bài tiếp theo.