Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là Technical PM tại một công ty fintech ở TP.HCM. Đội mobile cần lấy danh sách giao dịch của người dùng, đội data cần ghi nhận sự kiện thanh toán, còn đối tác bên ngoài muốn tích hợp để tra cứu số dư. Cả ba nhóm này đều "nói chuyện" với hệ thống của bạn qua một thứ duy nhất: REST API. Nếu API được thiết kế tốt, cả ba nhóm tự đọc tài liệu và tự tích hợp trong vài ngày. Nếu thiết kế tệ, bạn sẽ mất hàng tuần họp đi họp lại, sửa tới sửa lui, và mỗi lần thay đổi lại làm vỡ một tích hợp nào đó.
REST API là "hợp đồng giao tiếp" giữa các hệ thống. Là một PM kỹ thuật, bạn không cần tự tay code endpoint, nhưng bạn cần đủ hiểu để review thiết kế API, phát hiện quyết định sai từ sớm, và bảo vệ trải nghiệm của người tiêu thụ API. Một URL đặt sai tên hôm nay có thể trở thành nợ kỹ thuật mà bạn phải sống chung suốt nhiều năm, vì khi đối tác đã tích hợp rồi thì việc đổi rất tốn kém.
Bài này tập trung riêng vào các nguyên tắc thiết kế REST API — cách đặt URL, dùng HTTP method và status code, phân trang, xử lý lỗi, idempotency. Đây là nền tảng để sau này bạn so sánh REST với GraphQL (Bài 8), thiết kế versioning (Bài 23) hay rate limiting (Bài 24).
Khái niệm cốt lõi
REST (Representational State Transfer) không phải một công nghệ hay thư viện. Nó là một phong cách kiến trúc — một bộ nguyên tắc về cách tổ chức giao tiếp giữa client và server qua HTTP. Một API "RESTful" tốt giúp lập trình viên đoán được hành vi mà không cần đọc hết tài liệu.
Resource-oriented: URL là danh từ, không phải động từ
Nguyên tắc nền tảng nhất: API REST xoay quanh tài nguyên (resource), không phải hành động. Mỗi tài nguyên là một "danh từ" trong nghiệp vụ của bạn — đơn hàng, người dùng, sản phẩm — và được định danh bằng một URL.
URL nên là danh từ:
- Đúng:
GET /orders/123— lấy đơn hàng số 123. - Sai:
GET /getOrder?id=123— đây là tư duy gọi hàm (RPC), không phải REST.
- Dùng danh từ số nhiều cho tập hợp:
/orders,/users,/products. - Tài nguyên con lồng nhau thể hiện quan hệ:
/users/42/orders— các đơn hàng của user 42. - Tránh động từ trong URL như
/createOrder,/deleteUser. Nếu bạn thấy động từ, gần như chắc chắn thiết kế đang lệch khỏi REST.
HTTP method ánh xạ với hành động (CRUD)
Mỗi method mang một ngữ nghĩa cố định. Đây là phần mà PM cần nắm chắc khi review:
- GET — đọc dữ liệu, không được thay đổi gì trên server (an toàn, safe).
- POST — tạo tài nguyên mới, hoặc kích hoạt một xử lý.
- PUT — thay thế toàn bộ một tài nguyên đã tồn tại.
- PATCH — cập nhật một phần tài nguyên (chỉ vài trường).
- DELETE — xóa tài nguyên.
| Hành động | Method + URL |
|---|---|
| Lấy danh sách đơn | GET /orders |
| Lấy 1 đơn | GET /orders/123 |
| Tạo đơn mới | POST /orders |
| Cập nhật toàn bộ đơn | PUT /orders/123 |
| Cập nhật trạng thái | PATCH /orders/123 |
| Hủy đơn | DELETE /orders/123 |
Stateless: server không nhớ phiên làm việc
REST yêu cầu stateless — mỗi request phải chứa đủ thông tin để server xử lý, server không lưu ngữ cảnh giữa các request. Nghĩa là token xác thực, ngôn ngữ, tham số... phải gửi kèm trong mỗi lần gọi, chứ server không "nhớ" rằng request trước bạn là ai.
Vì sao điều này quan trọng với PM? Vì stateless là yếu tố giúp hệ thống scale ngang dễ dàng. Khi server không giữ trạng thái, ta có thể đặt 10 server giống hệt nhau sau một load balancer, request đi vào server nào cũng được. Nếu API lỡ lưu trạng thái trong bộ nhớ một server cụ thể, việc mở rộng sẽ vỡ. (Chi tiết về xác thực stateless như JWT sẽ học ở Bài 10.)
Idempotency: gọi lại nhiều lần vẫn an toàn
Một khái niệm cực kỳ quan trọng trong fintech và e-commerce: idempotency (tính bất biến khi lặp lại). Một thao tác idempotent nghĩa là gọi 1 lần hay 5 lần đều cho kết quả trạng thái giống nhau.
- GET, PUT, DELETE theo bản chất là idempotent.
- POST thì không — gọi
POST /ordershai lần sẽ tạo hai đơn hàng.
HTTP status code nói lên điều gì đã xảy ra
Status code là cách server trả lời "việc của bạn ra sao". Dùng đúng giúp client xử lý đúng:
- 2xx — thành công:
200 OK(đọc/cập nhật ổn),201 Created(tạo mới thành công),204 No Content(thành công, không trả nội dung — thường dùng cho DELETE). - 4xx — lỗi phía client:
400 Bad Request(dữ liệu gửi sai),401 Unauthorized(chưa xác thực),403 Forbidden(đã xác thực nhưng không có quyền),404 Not Found(không tìm thấy),409 Conflict(xung đột, ví dụ trùng dữ liệu),422 Unprocessable Entity(dữ liệu hợp lệ về cú pháp nhưng sai nghiệp vụ). - 5xx — lỗi phía server:
500 Internal Server Error,503 Service Unavailable.
200 OK kèm một body ghi {"error": "..."}. Đây là chống chỉ định — client tưởng thành công, hệ thống giám sát cũng không phát hiện được lỗi.Phân trang, lọc, sắp xếp qua query parameter
Với danh sách lớn, không bao giờ trả hết một lần. Dùng query parameter để phân trang và lọc, ví dụ: GET /orders?status=paid&page=2&limit=20&sort=-created_at. Lưu ý: lọc và phân trang nằm ở query string, còn URL path vẫn giữ là danh từ tài nguyên.
Tình huống thực tế
Ví dụ 1 — Tiki và bài học "động từ trong URL"
Giả định một đội tại sàn thương mại điện tử như Tiki ban đầu thiết kế API theo kiểu RPC: /getProductList, /addToCart, /checkoutNow, /cancelOrderById. Mỗi tính năng mới lại sinh thêm một endpoint động từ. Sau một năm, họ có hơn 200 endpoint với cách đặt tên không nhất quán — chỗ thì getX, chỗ thì fetchX, chỗ thì loadX. Lập trình viên mới mất cả tuần chỉ để hiểu nên gọi cái nào.
Khi đội mobile app thứ hai cần tích hợp, họ liên tục hỏi: "Lấy chi tiết sản phẩm là getProduct hay getProductDetail hay productInfo?" Không ai đoán được vì không có quy luật.
Bài học rút ra: Khi chuyển sang tư duy resource-oriented, toàn bộ thao tác trên sản phẩm gói gọn quanh /products và /products/{id} với các HTTP method khác nhau. Lập trình viên chỉ cần nhớ một danh từ và đoán được phần còn lại. Là PM, khi review thiết kế API, hễ thấy động từ trong URL, hãy đặt câu hỏi ngay — đó là tín hiệu cảnh báo sớm.
Ví dụ 2 — Cổng thanh toán và Idempotency-Key
Một cổng thanh toán như VNPAY hay một startup ví điện tử ở Đông Nam Á (giả định gọi là PayFast) gặp sự cố: khi khách bấm "Thanh toán" trên mạng 3G chập chờn, app gửi POST /payments nhưng không nhận được phản hồi do timeout. App tự động thử lại. Kết quả: khách bị trừ tiền hai lần cho cùng một đơn hàng 500.000đ. Mỗi ngày bộ phận CSKH nhận hàng chục khiếu nại kiểu này.
Giải pháp: yêu cầu client sinh một Idempotency-Key (ví dụ UUID) cho mỗi lần khởi tạo thanh toán và gửi kèm trong header. Server lưu key này. Khi request thứ hai mang cùng key tới, server nhận ra giao dịch đã xử lý và trả về chính kết quả cũ thay vì tạo giao dịch mới. Khách chỉ bị trừ một lần dù app thử lại bao nhiêu lần.
Bài học rút ra: Với mọi thao tác POST liên quan tới tiền hoặc tạo dữ liệu quan trọng, PM phải chủ động đặt câu hỏi: "Nếu client gọi lại do timeout thì sao?" Idempotency không phải tính năng "nice to have" — nó là yêu cầu bắt buộc trong fintech, và phải nằm trong acceptance criteria của bạn.
Ví dụ 3 — Grab và phân trang khi dữ liệu phình to
Một đội tại nền tảng gọi xe như Grab xây API GET /drivers/{id}/trips để app tài xế xem lịch sử chuyến đi. Lúc mới ra mắt, tài xế chỉ có vài chục chuyến nên API trả toàn bộ một lần — không vấn đề gì. Sáu tháng sau, một tài xế chăm chỉ tích lũy hơn 8.000 chuyến. Mỗi lần mở màn hình lịch sử, API trả về một mảng JSON khổng lồ vài megabyte, app đơ 4-5 giây, và server thì tốn bộ nhớ.
Họ sửa bằng cách thêm phân trang: GET /drivers/{id}/trips?limit=20&page=1, kèm metadata về tổng số trang. Màn hình tải tức thì, server nhẹ nhõm.
Bài học rút ra: Bất kỳ endpoint nào trả về một danh sách đều phải có phân trang ngay từ ngày đầu, kể cả khi dữ liệu hiện tại còn ít. Thêm phân trang về sau là một breaking change đau đớn vì mọi client đã tích hợp đều phải sửa. Là PM, hãy biến "danh sách phải có phân trang" thành một checklist mặc định.
Hướng dẫn từng bước
Khi bạn review hoặc đồng thiết kế một REST API cùng đội kỹ thuật, hãy đi theo trình tự sau:
- Xác định các tài nguyên (danh từ) cốt lõi. Liệt kê các thực thể nghiệp vụ chính: đơn hàng, người dùng, sản phẩm, thanh toán. Đây sẽ là các URL gốc. Nếu bạn đang dùng động từ để mô tả, hãy dịch nó về danh từ.
- Ánh xạ hành động vào HTTP method. Với mỗi tài nguyên, xác định cần CRUD nào và gán method tương ứng (GET/POST/PUT/PATCH/DELETE). Tránh tạo endpoint riêng cho mỗi hành động.
- Thiết kế quan hệ qua URL lồng nhau. Quan hệ "thuộc về" thể hiện qua path:
/users/{id}/orders. Đừng lồng quá 2 cấp —/users/1/orders/5/items/9/reviewslà dấu hiệu thiết kế quá phức tạp.
- Quy ước status code cho từng kịch bản. Liệt kê: thành công trả gì, không tìm thấy trả gì, không có quyền trả gì, dữ liệu sai trả gì. Thống nhất một bảng dùng chung cho cả API.
- Chuẩn hóa định dạng lỗi. Mọi lỗi nên có cùng cấu trúc body, ví dụ
{ "code": "INVALID_AMOUNT", "message": "Số tiền phải lớn hơn 0", "field": "amount" }. Client xử lý lỗi nhất quán nhờ trườngcodeổn định, cònmessageđể hiển thị.
- Thêm phân trang, lọc, sắp xếp cho mọi danh sách. Quyết định cơ chế (offset/page hay cursor) và luôn trả metadata về tổng số.
- Xem xét idempotency cho thao tác tạo. Với POST quan trọng, quyết định có cần Idempotency-Key không.
- Viết tài liệu và ví dụ. Mỗi endpoint cần request mẫu, response mẫu, và danh sách lỗi có thể xảy ra. Đây chính là "developer experience" mà bạn sẽ học sâu hơn ở Bài 20.
Lỗi thường gặp & mẹo
- Động từ trong URL.
/createOrder,/getUserByIdlà tư duy RPC. Mẹo: nếu URL của bạn chứa động từ, hãy thử viết lại bằng danh từ + HTTP method.
- Trả 200 cho cả lỗi. Trả
200 OKkèm{"success": false}khiến hệ thống giám sát mù tịt và client dễ xử lý sai. Luôn dùng status code đúng với kết quả thực.
- Lạm dụng status code 500. Dữ liệu người dùng gửi sai là lỗi 4xx (lỗi client), không phải 500. Dùng 500 bừa bãi khiến đội vận hành báo động giả và bỏ sót lỗi thật.
- Đặt tên không nhất quán. Chỗ
camelCase, chỗsnake_case; chỗ số nhiều, chỗ số ít. Hãy chốt một quy ước và áp dụng toàn bộ API.
- Quên phân trang. Như ví dụ Grab — danh sách không phân trang là quả bom hẹn giờ. Thêm về sau là breaking change.
- Endpoint "tổng hợp" kiểu RPC. Khi gặp nghiệp vụ phức tạp như "hoàn tiền", đừng vội tạo
/refund. Hãy mô hình hóa thành tài nguyên:POST /orders/123/refundstạo ra một tài nguyên "refund". Tư duy này giúp API nhất quán hơn.
- Mẹo cho PM: Bạn không cần phán xét code, nhưng hãy luôn hỏi ba câu khi review API: "Người tiêu thụ API có đoán được hành vi không? Lỗi được trả thế nào? Nếu client gọi lại thì có an toàn không?" Ba câu này bắt được phần lớn vấn đề thiết kế.
Bài tập thực hành
Giả sử bạn là Technical PM cho một startup giao đồ ăn ở Hà Nội. Đội kỹ thuật đề xuất bộ API sau cho tính năng đặt món:
GET /getRestaurants?city=hanoi
POST /createNewOrder
GET /orderStatus?orderId=789
POST /cancelOrder
GET /getAllReviewsForRestaurant?id=12
Hãy thực hiện:
- Viết lại cả 5 endpoint theo đúng nguyên tắc resource-oriented (danh từ + HTTP method đúng).
- Chỉ ra endpoint nào cần Idempotency-Key và giải thích vì sao.
- Thiết kế một bảng status code cho endpoint tạo đơn: liệt kê ít nhất 4 kịch bản (thành công, nhà hàng đã đóng cửa, món hết hàng, chưa đăng nhập) và status code tương ứng.
- Thêm phân trang cho endpoint danh sách đánh giá và mô tả response metadata bạn muốn trả về.
GET /restaurants?city=hanoi, POST /orders, GET /orders/789, DELETE /orders/789 (hoặc PATCH /orders/789 với trạng thái hủy), GET /restaurants/12/reviews.Tóm tắt
REST API là hợp đồng giao tiếp giữa các hệ thống, và là một Technical PM, bạn có trách nhiệm bảo vệ chất lượng của hợp đồng đó. Những điểm cốt lõi cần nhớ:
- Resource-oriented: URL là danh từ số nhiều (
/orders/123), hành động thể hiện qua HTTP method, không nhét động từ vào URL. - HTTP method có ngữ nghĩa cố định: GET đọc, POST tạo, PUT/PATCH cập nhật, DELETE xóa.
- Stateless: mỗi request tự chứa đủ thông tin, giúp hệ thống scale ngang.
- Idempotency: thao tác POST quan trọng (đặc biệt liên quan tiền) cần Idempotency-Key để chống tạo trùng khi client thử lại.
- Status code phải phản ánh đúng kết quả; đừng trả 200 cho lỗi, đừng lạm dụng 500.
- Danh sách luôn có phân trang ngay từ đầu để tránh breaking change sau này.