HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Request validation failed",
"status": 422,
"detail": "2 fields are invalid",
"instance": "/orders/req-7f3a",
"traceId": "4bf92f3577b34da6",
"errors": [
{ "field": "shippingAddress.postalCode", "code": "invalid_format" },
{ "field": "items[0].quantity", "code": "must_be_positive" }
]
}
API Design
draftedREST semantics, idempotency, pagination, versioning, error format (RFC 9457), rate limiting, webhooks, GraphQL, gRPC — thiết kế API sống được qua nhiều năm và nhiều client.
0/46 rated · mastery 0%
Bài học
12 bài · ~155 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~17 phút· 5 câuverify
HTTP semantics: method, status code và resource
Safe/idempotent method và ai dựa vào chúng để retry, PUT vs PATCH (JSON Merge Patch, JSON Patch), bảng status code cho create/async/validation/conflict, 401 vs 403 vs 404 với multi-tenant, và khi nào action endpoint như POST /orders/{id}/cancel là thiết kế đúng.
- 02~12 phút· 4 câuverify
Error format (RFC 9457) và quy ước dữ liệu JSON
Vì sao 200 + success:false phá vận hành, Problem Details (type/title/status/detail/instance + extension), mã lỗi ổn định cho UI như checkout, không lộ stack trace, và quy ước tiền, thời gian, ID 64-bit, enum trong JSON.
- 03~13 phút· 4 câuverify
Idempotency key cho POST
Vì sao timeout làm client không biết request đã chạy chưa, thiết kế Idempotency-Key (store, scope theo tenant, request hash, trạng thái processing/done, TTL), xử lý retry đồng thời và crash sau khi đã charge, và cách điều tra sự cố đơn trùng 0,3% trên mobile.
- 04~12 phút· 5 câuverify
Pagination, filtering & sorting
Offset vs keyset (cursor) pagination chạy thật trên Postgres: vì sao offset trùng/mất item và chậm dần, sort key unique với tie-breaker, cursor opaque có ký HMAC, filter/sort theo allow-list, và search_after + PIT khi API đứng trước Elasticsearch.
- 05~11 phút· 5 câuverify
HTTP caching, ETag và optimistic concurrency
Cache-Control (max-age, s-maxage, no-cache vs no-store, private), Vary, vì sao CDN trả giỏ hàng của user A cho user B, ETag + If-None-Match cho 304, If-Match + version column cho 412/428 chống lost update, và thiết kế Address Book với đúng một địa chỉ mặc định.
- 06~13 phút· 6 câuverify
Versioning, breaking change và API governance
Breaking change hiển nhiên và ẩn (enum mới, đổi default, null vs vắng field), tolerant reader chạy thật với zod, versioning qua URL/header/ngày, Deprecation + Sunset header, expand-contract khi đổi tên field cho 40 client, contract-first OpenAPI với breaking-change diff trong CI, giữ contract khi tách monolith, và chuẩn hoá convention cho 30 service.
- 07~14 phút· 4 câuverify
Rate limiting: thuật toán, key và distributed
Rate limit theo key nào (IP, user, API key, tenant, route), 429 + Retry-After + RateLimit header, fixed/sliding window, token/leaky bucket đo thật, token bucket nguyên tử bằng Redis Lua, vì sao limiter in-memory cho qua 3 lần limit, trust proxy và X-Forwarded-For, và cách điều tra khi partner kêu bị 429.
- 08~12 phút· 4 câuverify
Long-running operation, bulk và file upload
202 Accepted + job resource + polling với Retry-After cho export 2 triệu đơn, bulk update 10.000 sản phẩm theo all-or-nothing hoặc best-effort (SAVEPOINT, kết quả từng item), và upload qua presigned URL/POST policy lên S3 với các gotcha của AWS SDK v3.
- 09~13 phút· 5 câuverify
Resilience và SLO cho API
Khi downstream chậm từ 200 ms lên 8 s: timeout và deadline propagation, retry có budget và jitter, circuit breaker, bulkhead, load shedding, fallback (đo thật từng pattern), API gateway vs BFF, và cách chọn SLO, SLA, limit công khai cho một public API.
- 10~12 phút· 4 câuverify
Webhooks và realtime push
Thiết kế hệ thống gửi webhook (outbox, retry với backoff nhiều ngày, dead letter, replay), ký HMAC trên raw body + timestamp và bug JSON.stringify chạy thật, chống replay, dedupe theo event id, ordering, xoay secret không mất event, và chọn SSE, WebSocket, long polling hay webhook để đẩy trạng thái đơn hàng.
- 11~13 phút· 6 câuverify
GraphQL và gRPC
Khi nào REST, GraphQL hay gRPC; N+1 và DataLoader đo thật (7 query còn 2), những gì DataLoader không giải quyết, bảo vệ GraphQL public (depth, cost, alias, persisted queries), quy tắc tiến hoá Protobuf chạy thật với protobufjs (field number, reserved, presence), deadline của gRPC, và chọn federation, BFF hay REST aggregation.
- 12~13 phút· 5 câuverify
Authorization và API cho multi-tenant
Tenant lấy từ đâu và vì sao server phải verify membership, kiểm tra hai lớp (permission ở middleware + resource ở data layer, RLS làm lưới an toàn), 404 cho tenant khác, role khác nhau theo tenant, ma trận role × endpoint bắt được một lỗi BOLA thật, resource chia sẻ, Google OAuth → token nội bộ, và thiết kế Admin API vs Storefront API.
Tóm tắt & cheat sheet của track
TL;DR
- API là contract lâu dài: một khi client đã phụ thuộc, mọi thay đổi đều có giá. Senior interview hỏi về evolution (versioning, backward compatibility, deprecation) nhiều hơn là "REST là gì".
- Nắm chắc HTTP semantics (RFC 9110): method nào safe, method nào idempotent, status code nào nói gì — đây là nền cho retry, caching và proxy hoạt động đúng.
- Mạng không tin cậy → mọi mutation quan trọng (payment, checkout) cần idempotency key; mọi list endpoint cần pagination ổn định (cursor/keyset); mọi lỗi cần format thống nhất (Problem Details, RFC 9457).
- Bảo vệ hệ thống: rate limiting (token bucket / sliding window), timeout, quota theo tenant; bảo vệ client:
429+Retry-After. - REST vs GraphQL vs gRPC không phải "cái nào tốt hơn" mà là ai là client: public/partner → REST + OpenAPI; nhiều UI cần shape khác nhau → GraphQL/BFF; service-to-service nội bộ, latency thấp, streaming → gRPC.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Safe method | Không thay đổi state phía server (về mặt ngữ nghĩa) | GET, HEAD, OPTIONS |
| Idempotent method | Gọi N lần = gọi 1 lần (về state) | PUT /users/1, DELETE /users/1; POST thì không |
| Idempotency key | Client gửi key duy nhất để server dedupe retry của một POST | Idempotency-Key: 5f1c… trên POST /payments |
| Cursor / keyset pagination | Trang kế tiếp dựa trên vị trí (giá trị sort cuối), không dựa trên offset | ?after=eyJpZCI6MTIzfQ&limit=50 |
| Problem Details | Format lỗi chuẩn application/problem+json (RFC 9457) | { "type", "title", "status", "detail", "instance" } |
| Conditional request | Dùng ETag + If-Match / If-None-Match cho cache & optimistic concurrency | PUT với If-Match: "v7" → 412 nếu đã đổi |
| Rate limiting | Giới hạn số request theo key (user, tenant, IP) trong cửa sổ thời gian | 429 Too Many Requests + Retry-After: 30 |
| Webhook | Server gọi ngược tới client khi có event; cần ký (HMAC) + retry + dedupe | X-Signature: sha256=… |
| Breaking change | Thay đổi làm client cũ hỏng | Đổi tên field, đổi kiểu, thêm required field vào request |
Cơ chế hoạt động
Idempotency key cho POST /payments
Lựa chọn pagination
Vòng đời một thay đổi API (evolution)
Trade-offs
| Tiêu chí | REST (JSON/HTTP) | GraphQL | gRPC |
|---|---|---|---|
| Client phù hợp | Public, partner, browser, mobile | Nhiều UI cần shape khác nhau | Service-to-service nội bộ |
| HTTP caching | Tốt (GET + ETag + CDN) | Khó (thường POST, 1 endpoint) — cần persisted queries | Không dùng HTTP cache |
| Over/under-fetching | Có (giải bằng fields/expand, BFF) | Client chọn field | Schema cố định theo RPC |
| Contract | OpenAPI | SDL schema, introspection | .proto, codegen mạnh |
| Streaming | SSE/WebSocket riêng | Subscriptions | Native (client/server/bidi) |
| Rủi ro chính | Endpoint sprawl, versioning | N+1 resolver, query quá phức tạp (DoS) | Browser cần gRPC-Web/proxy; debug khó hơn |
| Versioning | Ưu | Nhược |
|---|---|---|
URL /v2/orders | Rõ ràng, dễ route, dễ cache | Nhân đôi routes; "big bang" version |
| Header / media type | URL sạch, version theo resource | Khó test bằng browser, dễ quên ở proxy/cache (Vary) |
| Không version, chỉ additive + deprecation | Ít overhead | Cần kỷ luật contract testing rất cao |
Pitfalls
- Dùng
200 OKcho mọi thứ kèm{ "success": false }→ phá monitoring, retry logic và cache. - Retry
POSTkhông có idempotency key → charge/đơn hàng trùng. - Offset pagination trên bảng lớn đang được ghi → trùng/mất item,
OFFSET 1000000quét cả triệu row. - Trả thẳng entity DB ra API → lộ field nội bộ, mỗi đổi schema DB thành breaking change.
404vs403cho resource của tenant khác: trả403làm lộ sự tồn tại (enumeration) — thường trả404.- Webhook không ký hoặc so sánh chữ ký bằng
===(timing attack), không chống replay (timestamp). - GraphQL public không giới hạn depth/complexity → một query có thể hạ DB.
- Rate limit in-memory trên nhiều instance → limit thực tế = N × limit.
Cheat sheet
- Safe:
GET HEAD OPTIONS TRACE· Idempotent: safe +PUT DELETE· Không idempotent:POST, (thường)PATCH. 201+Locationkhi tạo ·202cho job dài (trả status URL) ·204không body ·304Not Modified.400malformed ·401chưa xác thực (kèmWWW-Authenticate) ·403đã xác thực nhưng không có quyền ·404·409conflict state ·412precondition failed ·422validation ·429rate limit.- Lỗi:
application/problem+json(RFC 9457 thay RFC 7807). - Pagination: cursor opaque (base64 của sort key), sort key phải unique (
created_at, id). - Breaking: xoá/đổi tên field, đổi type, thêm required input, đổi ý nghĩa enum, đổi default. Non-breaking: thêm field optional, thêm endpoint — nhưng client phải tolerant với field/enum lạ.
- Webhook: HMAC-SHA256 trên raw body + timestamp, constant-time compare, at-least-once → consumer dedupe theo event id.
References
- RFC 9110 — HTTP Semantics ↗
- RFC 9457 — Problem Details for HTTP APIs ↗
- RFC 6585 — Additional HTTP Status Codes (429) ↗
- RFC 8594 — The Sunset HTTP Header Field ↗
- RFC 9745 — The Deprecation HTTP Response Header Field ↗
- IETF draft — The Idempotency-Key HTTP Header Field ↗
- IETF draft — RateLimit header fields for HTTP ↗
- OpenAPI Specification ↗
- GraphQL — Best practices (pagination, caching) ↗
- gRPC — Core concepts ↗
- Google API Design Guide ↗
- Stripe — Idempotent requests ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 46 / 46
-- PostgreSQL
SELECT id, created_at, total
FROM orders
WHERE tenant_id = $1
AND (created_at, id) < ($2, $3) -- từ cursor
ORDER BY created_at DESC, id DESC
LIMIT 51; -- limit + 1
-- SQL Server: không có row value comparison
-- AND (created_at < @c OR (created_at = @c AND id < @id))
const cursor = Buffer.from(JSON.stringify({ c: last.createdAt, id: last.id })).toString('base64url');
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody: Buffer, ts: string, sig: string, secret: string) {
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window
const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(sig);
return a.length === b.length && timingSafeEqual(a, b);
}
message Order {
reserved 3; // từng là `string coupon = 3;`
reserved "coupon";
string id = 1;
int64 total_cents = 2;
optional string note = 4; // có presence
}
const hits = new Map<string, { count: number; resetAt: number }>();
export function rateLimit(limit = 100, windowMs = 60_000) {
return (req: Request, res: Response, next: NextFunction) => {
const key = req.ip;
const now = Date.now();
const entry = hits.get(key) ?? { count: 0, resetAt: now + windowMs };
if (now > entry.resetAt) { entry.count = 0; entry.resetAt = now + windowMs; }
entry.count++;
hits.set(key, entry);
if (entry.count > limit) return res.status(429).end();
next();
};
}
// GET /orders?page=3&size=20
const rows = await db.query(
`SELECT * FROM orders WHERE tenant_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`,
[tenantId, size, (page - 1) * size],
);
app.use(express.json());
app.post('/webhooks/payments', (req, res) => {
const signature = req.header('x-signature')!;
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expected) return res.status(401).end();
handle(req.body);
res.sendStatus(200);
});