Thiết kế API
Cách thiết kế API trong phỏng vấn: REST theo resource, so sánh REST, GraphQL và gRPC, phân trang offset và cursor, idempotency key, versioning, định dạng lỗi, xác thực bằng session, JWT, OAuth, rate limit, kèm ví dụ NestJS và Express.
API là gì trong một bài thiết kế
API là hợp đồng giữa client và server: client được gọi những gì, gửi dữ liệu dạng nào, nhận lại gì, lỗi trông ra sao. Trong phỏng vấn, bước thiết kế API thường đến ngay sau khi chốt yêu cầu chức năng. Mỗi yêu cầu chức năng thường thành một hoặc hai endpoint. Microservices Patterns coi việc định nghĩa API là một phần của thiết kế: API gồm các thao tác và sự kiện mà client dùng, và cần có kế hoạch cho việc API thay đổi theo thời gian.1
Một API tốt có bốn tính chất:
- Dễ đoán: nhìn tên endpoint là biết nó làm gì.
- An toàn khi gọi lại: mạng chập chờn, client retry không làm trừ tiền hai lần.
- Mở rộng được: thêm trường mới không làm hỏng client cũ (thay đổi tương thích ngược; Microservices Patterns ghi rằng không thể ép mọi client nâng cấp cùng lúc).2
- Có giới hạn: không ai gọi được không giới hạn, không trả về một triệu dòng trong một response.
Vì sao quan trọng trong phỏng vấn
API là chỗ người phỏng vấn kiểm tra bạn đã hiểu bài toán chưa. Liệt kê được POST /bookings với body gồm eventId, seatIds cho thấy bạn biết thực thể chính và luồng chính. Thêm Idempotency-Key cho thấy bạn nghĩ tới retry. Thêm cursor cho thấy bạn nghĩ tới dữ liệu lớn.
Bạn không cần viết đặc tả đầy đủ. Ba tới sáu endpoint chính, mỗi endpoint một dòng, đủ để cả hai bên thống nhất. Dành 3–5 phút cho phần này, rồi chuyển sang kiến trúc (gợi ý của khoá học; Grokking khuyên định nghĩa API ở mức cao để hiểu dữ liệu và tương tác của hệ thống).3
Trong phỏng vấn: Đừng đưa userId vào body hay URL cho các thao tác của chính người dùng. Server lấy user từ token. Viết POST /tweets thay vì POST /users/123/tweets. Người phỏng vấn hay để ý chi tiết này vì nó liên quan tới bảo mật. Microservices Patterns mô tả API gateway xác thực client rồi chuyển danh tính người dùng cho service bằng token, thay vì để client tự khai.4
REST theo resource
REST tổ chức API quanh resource (danh từ) và dùng HTTP method làm hành động (động từ). Đây là lựa chọn mặc định cho API công khai và API từ client tới backend. Microservices Patterns định nghĩa resource là một đối tượng nghiệp vụ như Customer hoặc Product (hay một tập hợp chúng), được tham chiếu bằng URL và thao tác bằng các HTTP verb.5 Building Microservices ghi GET đọc resource theo cách idempotent còn POST tạo resource mới.6
| Method | Ý nghĩa | Idempotent | Ví dụ |
|---|---|---|---|
| GET | Đọc | Có | GET /events/42 |
| POST | Tạo mới, hoặc hành động không idempotent | Không | POST /events/42/bookings |
| PUT | Thay toàn bộ resource | Có | PUT /users/me/settings |
| PATCH | Sửa một phần | Tùy cách làm | PATCH /posts/7 |
| DELETE | Xóa | Có | DELETE /posts/7 |
Bảng trên: GET và POST có trong sách; tính idempotent của PUT, PATCH, DELETE là kiến thức HTTP chung, chưa có sách trong thư viện mô tả.7
Quy tắc đặt tên (quy ước của khoá học):
- Danh từ số nhiều:
/events,/bookings. - Lồng tối đa một cấp khi có quan hệ sở hữu rõ ràng:
/events/42/seats. Sâu hơn thì tách ra:/seats?eventId=42. - Hành động khó biểu diễn bằng CRUD thì tạo resource cho nó: hủy đơn là
POST /orders/9/cancellationhoặc chấp nhận một động từPOST /orders/9:cancel. Chọn một kiểu và dùng nhất quán. - Lọc, sắp xếp, phân trang qua query string:
GET /posts?author=me&sort=-createdAt&limit=20.
Mã trạng thái cần dùng đúng (Building Microservices dùng 202 cho "đã nhận, chưa xử lý" và 201 khi hoàn tất;8 Grokking trả 429 khi vượt giới hạn;9 các mã còn lại là quy ước HTTP chung7): 200 đọc thành công, 201 tạo xong, 202 đã nhận và xử lý sau (job dài), 204 không có body, 400 dữ liệu sai, 401 chưa đăng nhập, 403 không có quyền, 404 không tồn tại, 409 xung đột (ghế đã bị giữ), 429 gọi quá nhiều, 5xx lỗi server.
Ví dụ NestJS cho bài đặt vé:
@Controller("events/:eventId/bookings")
@UseGuards(JwtAuthGuard)
export class BookingController {
constructor(private readonly bookings: BookingService) {}
@Post()
@HttpCode(201)
create(
@Param("eventId") eventId: string,
@Body() dto: CreateBookingDto,
@Headers("idempotency-key") key: string,
@Req() req: AuthedRequest,
) {
return this.bookings.reserve(req.user.id, eventId, dto.seatIds, key);
}
}
REST, GraphQL và gRPC
Microservices Patterns ch.3 chia giao tiếp giữa service thành đồng bộ (REST, gRPC) và bất đồng bộ (message). Ch.8 bàn về API cho client bên ngoài, nơi GraphQL thường xuất hiện ở tầng API gateway.1011 Sách nêu gRPC là giao thức nhị phân dựa trên message, định nghĩa API bằng Protocol Buffers, chạy trên HTTP/2 và hỗ trợ nhiều ngôn ngữ; Protocol Buffers giúp việc tiến hoá API dễ hơn.12 Về GraphQL, sách nói client có thể được phép chỉ định dữ liệu cần (tham số expand, fields) hoặc dùng Backends for frontends, và GraphQL được cân nhắc khi cần phục vụ nhiều loại client.11 Ba lựa chọn đồng bộ so sánh như sau (các dòng về cache, streaming, công cụ là kiến thức chung chưa có sách):7
| Tiêu chí | REST | GraphQL | gRPC |
|---|---|---|---|
| Mô hình | Resource + HTTP method | Một endpoint, client gửi query mô tả dữ liệu cần | Gọi hàm từ xa, định nghĩa bằng .proto |
| Định dạng | JSON | JSON | Protobuf nhị phân |
| Transport | HTTP/1.1 hoặc HTTP/2 | HTTP | HTTP/2 |
| Lấy thừa, lấy thiếu | Hay gặp, phải gọi nhiều endpoint | Client lấy đúng trường cần trong một lần | Theo hợp đồng cố định |
| Streaming | Qua SSE, WebSocket | Subscription | Có sẵn: server, client, hai chiều |
| Cache HTTP, CDN | Dễ, theo URL | Khó, mọi request là POST tới /graphql | Không dùng được |
| Công cụ, debug | curl, trình duyệt | GraphiQL, cần thư viện | Cần sinh code, grpcurl |
| Hợp với | API công khai, client web và mobile | Nhiều loại client cần dữ liệu khác nhau, BFF | Service nội bộ gọi nhau, cần nhanh và có kiểu chặt |
Cách chọn thường gặp trong một hệ thống:
Lưu ý (kiến thức chung, chưa có sách trong thư viện):7 GraphQL dễ sinh N+1 query: lấy 20 bài viết, mỗi bài lại query tác giả. Dùng DataLoader để gom thành một query WHERE id IN (...). Nếu chọn GraphQL, nói luôn câu này.
Các chi tiết làm API chạy được ở quy mô lớn
Phân trang: offset và cursor
| Tiêu chí | Offset (?page=5&limit=20) | Cursor (?after=abc&limit=20) |
|---|---|---|
| Cách làm | LIMIT 20 OFFSET 80 | WHERE (created_at, id) < (:t, :id) ORDER BY ... LIMIT 20 |
| Nhảy tới trang bất kỳ | Được | Không |
| Hiệu năng trang sâu | Chậm dần, DB vẫn phải đọc qua các dòng bị bỏ | Ổn định, đi thẳng vào index |
| Dữ liệu thêm mới khi đang lướt | Bị lặp hoặc bỏ sót | Ổn định |
| Hợp với | Bảng quản trị, kết quả nhỏ | Feed, chat, comment, mọi danh sách dài |
Bảng so sánh offset và cursor là kiến thức chung về database và API, chưa có sách trong thư viện;7 Building Microservices chỉ nhắc API có thể cho phép lấy theo lô hoặc duyệt qua toàn bộ danh sách.8 Cursor nên là chuỗi mã hóa (base64 của createdAt và id) để client không tự ghép. Response trả kèm nextCursor, null khi hết.
app.get("/posts", async (req, res) => {
const limit = Math.min(Number(req.query.limit) || 20, 100);
const after = req.query.after ? decodeCursor(String(req.query.after)) : null;
const rows = await db.query(
`SELECT id, body, created_at FROM posts
WHERE ($1::timestamptz IS NULL OR (created_at, id) < ($1, $2))
ORDER BY created_at DESC, id DESC LIMIT $3`,
[after?.createdAt ?? null, after?.id ?? null, limit + 1],
);
const page = rows.slice(0, limit);
const next = rows.length > limit ? encodeCursor(page[page.length - 1]) : null;
res.json({ data: page, nextCursor: next });
});
Lấy limit + 1 dòng để biết còn trang sau mà không cần COUNT(*).
Idempotency key
Client gửi POST /payments, mạng đứt trước khi nhận response. Client không biết server đã trừ tiền chưa, nên retry. Không có biện pháp gì, khách bị trừ hai lần.
Microservices Patterns giải thích một thao tác là idempotent nếu gọi nhiều lần cho cùng kết quả, ví dụ tạo đơn với ID do client cung cấp, và message handler idempotent thì xử lý trùng vô hại.13 Cách giải: client sinh một UUID cho mỗi thao tác và gửi trong header Idempotency-Key. Server lưu kết quả theo key. Lần gọi lại với cùng key trả về đúng kết quả cũ.
Cách dùng header Idempotency-Key và Redis SET NX là kỹ thuật của khoá học, chưa có sách trong thư viện mô tả.7 Chi tiết cần nói: key gắn với user để người khác không dùng lại được; nếu key đang ở trạng thái processing thì trả 409 hoặc chờ; giữ key 24 giờ; trong DB nên có thêm unique constraint trên key để chặn cả khi Redis mất dữ liệu.
Versioning
| Cách | Ví dụ | Ghi chú |
|---|---|---|
| Trong URL | /v1/orders | Dễ thấy, dễ định tuyến, phổ biến nhất |
| Header | Accept: application/vnd.shop.v2+json | URL gọn, khó test bằng trình duyệt |
| Không version, chỉ thêm | Thêm trường mới, không xóa trường cũ | Cách tốt nhất cho thay đổi nhỏ |
Microservices Patterns khuyên dùng semantic versioning (MAJOR.MINOR.PATCH, MAJOR tăng khi có thay đổi không tương thích) và nêu rằng client thường không thể nâng cấp đồng loạt với service.2 Quy tắc: thêm trường tùy chọn thì không cần version mới. Đổi tên, xóa trường, đổi kiểu dữ liệu thì cần. Mobile app cũ sống rất lâu trên máy người dùng, nên version cũ thường phải giữ hàng tháng.
Định dạng lỗi
Dùng một định dạng duy nhất cho mọi lỗi (khuyến nghị của khoá học; chuẩn Problem Details chưa có sách trong thư viện)7 hoặc đơn giản hơn:
{
"error": {
"code": "SEAT_ALREADY_HELD",
"message": "Ghế A12 đang được người khác giữ.",
"details": { "seatId": "A12", "heldUntil": "2026-11-03T10:15:00Z" },
"requestId": "req_8f2c1a"
}
}
code cho máy đọc và ổn định giữa các version, message cho người đọc, requestId để tra log. Không trả stack trace ra ngoài.
Xác thực và rate limit
Session, JWT, OAuth
| Cách | Hoạt động | Ưu | Nhược | Hợp với |
|---|---|---|---|---|
| Session cookie | Server tạo session id, lưu trong Redis, gửi cookie HttpOnly | Thu hồi ngay được, cookie an toàn trước XSS | Mỗi request tra Redis, khó dùng cho mobile và service khác domain | Web app một domain |
| JWT | Token tự chứa user id, quyền, hạn dùng, có chữ ký | Không cần tra cứu, service nào có public key cũng xác minh được | Khó thu hồi trước hạn, token to | Mobile, microservices |
| OAuth 2.0 / OIDC | Uỷ quyền qua bên thứ ba (Google, Facebook) hoặc auth server riêng, trả access token | Không giữ mật khẩu, đăng nhập một lần | Luồng phức tạp hơn | Đăng nhập bằng mạng xã hội, cấp quyền cho app bên thứ ba |
Microservices Patterns mô tả session token (thường là cookie) với session lưu ở server, opaque token cần gọi dịch vụ bảo mật để xác thực nên tăng độ trễ, và JWT tự chứa nên không cần tra cứu nhưng không thể thu hồi từng token, giải pháp là cấp JWT hạn ngắn.4 OAuth 2.0 là giao thức uỷ quyền, thường dùng qua dịch vụ hoặc framework có sẵn.14 Các ưu nhược chi tiết trong bảng trên là đánh giá của khoá học. Cách làm phổ biến: access token JWT hạn ngắn (5–15 phút) cộng refresh token hạn dài lưu ở server, có thể thu hồi. API gateway xác minh JWT một lần và chuyển user id cho service phía sau.
export function auth(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: { code: "UNAUTHENTICATED" } });
try {
req.user = jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] }) as UserClaims;
next();
} catch {
res.status(401).json({ error: { code: "TOKEN_INVALID" } });
}
}
Rate limit
Grokking ch.19 mô tả rate limiter đặt trước API: xác định client (theo user id, API key hoặc IP), so với quy tắc, quá giới hạn thì trả 429 Too Many Requests. Các thuật toán hay dùng là token bucket, leaking bucket, fixed window counter, sliding window log, sliding window counter.15
Những gì nên đưa vào API (tên header cụ thể là quy ước của khoá học;7 Grokking nói rate limiter nằm ở lớp API và đếm bằng key-value store):9
- Header cho client biết giới hạn:
RateLimit-Limit,RateLimit-Remaining,Retry-Afterkhi trả 429. - Giới hạn khác nhau theo loại endpoint: đọc thoải mái, đăng nhập và gửi OTP rất chặt.
- Đặt ở API gateway, đếm trong Redis để mọi instance dùng chung một bộ đếm.
Chi tiết thiết kế xem bài Rate limiter.
Con số và sizing
Các con số dưới đây là ước lượng của khoá học (quy tắc ước lượng của khoá học, cần load test; nguồn công thức xem Chọn server),16 dùng để nói chuyện trong phỏng vấn. Các số về kích thước response, JWT, Redis không lấy từ sách.
| Thông số | Giá trị ước lượng |
|---|---|
| API Node.js I/O-bound | ~500 req/s mỗi vCPU, 2 vCPU · 4 GB ≈ 1.000 req/s |
| Số instance | max(2, ceil(peak / 600)) khi chạy ở ≤ 60% |
| Kích thước response JSON hợp lý | 1–50 KB, nén gzip giảm 70–80% |
| Page size mặc định | 20, tối đa 100 |
| Xác minh JWT RS256 | Vài chục micro giây, không đáng kể |
| Tra session trong Redis | ~0,5–1 ms trong cùng data center |
| Idempotency key trong Redis | ~200 B mỗi key, 10 triệu key/ngày ≈ 2–3 GB |
Ví dụ: 10 triệu người dùng hoạt động mỗi ngày, mỗi người 50 request. Trung bình 500 triệu / 86.400 ≈ 5.800 req/s. Peak ×5 ≈ 29.000 req/s. Số instance ≈ 29.000 / 600 ≈ 49 instance 2 vCPU. Grokking dùng mốc ~8.000 RPS mỗi server khi tính nhẩm và nêu máy chỉ phục vụ nội dung tĩnh từ RAM có thể tới khoảng 500 nghìn RPS còn tác vụ nặng như xử lý ảnh chỉ khoảng 50 RPS; xem 8.000 như giới hạn trên.17
Sai lầm hay gặp
(Các lỗi dưới đây là kinh nghiệm của khoá học, đối chiếu thêm với các mục có nguồn ở trên.)7
- Động từ trong URL cho mọi thứ:
/getUser,/createOrder. Dùng resource và method. - Trả về toàn bộ danh sách không phân trang. Ngày đầu có 50 dòng, một năm sau có 5 triệu dòng.
- Dùng offset cho feed vô hạn. Trang sâu chậm, bài mới chèn vào làm lặp bài.
- Không có idempotency cho thao tác tiền và đặt chỗ. Retry tự động của client, của load balancer, của thư viện HTTP sẽ gây trùng.
- Tin dữ liệu client gửi lên. Giá,
userId, quyền phải lấy từ server và token. - JWT hạn 30 ngày không có cách thu hồi. Token bị lộ là mất quyền kiểm soát 30 ngày.
- Trả 200 kèm
{"success": false}. Load balancer, monitoring và retry đều dựa vào status code.
Sai lầm hay gặp: Chọn GraphQL "vì nó hiện đại" cho một bài chỉ có một client và năm endpoint. Hãy nói lý do cụ thể: nhiều client cần hình dạng dữ liệu khác nhau, hoặc màn hình cần gộp dữ liệu từ nhiều service.
Khi nào nhắc tới trong phỏng vấn
| Bài | Điểm API cần nói |
|---|---|
| Đặt vé sự kiện | POST giữ ghế trả 409 khi xung đột, idempotency khi thanh toán |
| Thanh toán | Idempotency key bắt buộc, webhook có chữ ký, trạng thái 202 |
| News feed | Cursor pagination theo thời gian và id |
| Chat | REST để lấy lịch sử theo cursor, WebSocket để nhận tin mới |
| Rút gọn URL | POST /urls, GET /{code} trả 301 hoặc 302 |
| Rate limiter | Trả 429, header Retry-After, quy tắc theo user và endpoint |
| Dropbox | API cấp URL upload đã ký, upload theo chunk có thể tiếp tục |
| Flash sale | Rate limit chặt, idempotency khi đặt hàng |
| ChatGPT | Stream response, giới hạn token theo user |
| Lập lịch tác vụ | 202 Accepted kèm job id, endpoint tra trạng thái |
Bảng trên là gợi ý của khoá học; nguồn của từng ý nằm ở bài thiết kế tương ứng.16 Cách nhìn API gateway chịu trách nhiệm xác thực và định tuyến có trong Microservices Patterns ch.8.18
Đọc tiếp: Mạng cơ bản cho tầng truyền tải bên dưới, API gateway cho xác thực và rate limit ở rìa hệ thống, Quy trình nhiều bước cho API của các thao tác kéo dài.
Nguồn trích dẫn
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 Interprocess communication · §3.1.2 Defining APIs in a microservice architecture và §3.1.3 Evolving APIs. ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.1.3 Evolving APIs (mục "Use semantic versioning"). ↩ ↩2
-
Grokking Modern System Design · ch.2 System Design Interviews · bài "How to Perform Well", mục "Discuss the components" (định nghĩa API người dùng ở mức cao). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.11 Developing production-ready services · §11.1.2 Implementing security in a microservice architecture (mục "Using JWTs to pass user identity and roles"; session token ở §11.1.1). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.2.1 Using REST. ↩
-
Building Microservices (file .zip, tải về giải nén) · ch.4 Integration · mục "REST and HTTP". ↩
-
Chưa có nguồn trong thư viện: không sách nào trong thư viện mô tả tính idempotent của PUT/PATCH/DELETE, ý nghĩa đầy đủ các mã trạng thái, so sánh cache giữa REST và GraphQL, N+1 và DataLoader, phân trang cursor, header Idempotency-Key, Problem Details hay tên header rate limit; các phần này chỉ nêu khái niệm cơ bản. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Building Microservices (file .zip, tải về giải nén) · ch.4 Integration · phần bàn về gọi theo lô (batch): trả HTTP 202 cho yêu cầu đã nhận, sau đó 201 Created khi xong; cũng nêu việc lấy theo danh sách ID hoặc duyệt qua toàn bộ. ↩ ↩2
-
Grokking Modern System Design · ch.19 Rate Limiter · bài thiết kế rate limiter, phần trả HTTP 429 Too Many Requests khi vượt giới hạn. ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.1.1 Interaction styles và §3.2 Communicating using the synchronous Remote procedure invocation pattern. ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.8 External API patterns · §8.3.3 Implementing an API gateway using GraphQL (và §8.1 API design issues). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.2.2 Using gRPC và §3.1.4 Message formats. ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.6 Handling duplicate messages (mục "Writing idempotent message handlers"). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.11 · §11.1.2 (mục "Using OAuth 2.0 in a microservice architecture"). ↩
-
Grokking Modern System Design · ch.19 Rate Limiter · phần thuật toán: Token bucket, Leaking bucket, Fixed window counter, Sliding window log, Sliding window counter. ↩
-
Quy tắc ước lượng của khoá học, xem bài Chọn server. Không phải số liệu trích từ sách. ↩ ↩2
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · bài "Put Back-of-the-envelope Numbers in Perspective", mục "Requests estimation". ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.8 External API patterns · §8.2 The API gateway pattern. ↩