Skip to content
DevRecall
← Roadmap
08

API Design

drafted

REST 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.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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. 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. 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. 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ệmMột câuVí dụ ngắn
Safe methodKhông thay đổi state phía server (về mặt ngữ nghĩa)GET, HEAD, OPTIONS
Idempotent methodGọi N lần = gọi 1 lần (về state)PUT /users/1, DELETE /users/1; POST thì không
Idempotency keyClient gửi key duy nhất để server dedupe retry của một POSTIdempotency-Key: 5f1c… trên POST /payments
Cursor / keyset paginationTrang 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 DetailsFormat lỗi chuẩn application/problem+json (RFC 9457){ "type", "title", "status", "detail", "instance" }
Conditional requestDùng ETag + If-Match / If-None-Match cho cache & optimistic concurrencyPUT với If-Match: "v7" → 412 nếu đã đổi
Rate limitingGiới hạn số request theo key (user, tenant, IP) trong cửa sổ thời gian429 Too Many Requests + Retry-After: 30
WebhookServer gọi ngược tới client khi có event; cần ký (HMAC) + retry + dedupeX-Signature: sha256=…
Breaking changeThay đổ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)GraphQLgRPC
Client phù hợpPublic, partner, browser, mobileNhiều UI cần shape khác nhauService-to-service nội bộ
HTTP cachingTốt (GET + ETag + CDN)Khó (thường POST, 1 endpoint) — cần persisted queriesKhông dùng HTTP cache
Over/under-fetchingCó (giải bằng fields/expand, BFF)Client chọn fieldSchema cố định theo RPC
ContractOpenAPISDL schema, introspection.proto, codegen mạnh
StreamingSSE/WebSocket riêngSubscriptionsNative (client/server/bidi)
Rủi ro chínhEndpoint sprawl, versioningN+1 resolver, query quá phức tạp (DoS)Browser cần gRPC-Web/proxy; debug khó hơn
VersioningƯuNhược
URL /v2/ordersRõ ràng, dễ route, dễ cacheNhân đôi routes; "big bang" version
Header / media typeURL sạch, version theo resourceKhó test bằng browser, dễ quên ở proxy/cache (Vary)
Không version, chỉ additive + deprecationÍt overheadCần kỷ luật contract testing rất cao

Pitfalls

  • Dùng 200 OK cho mọi thứ kèm { "success": false } → phá monitoring, retry logic và cache.
  • Retry POST khô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 1000000 qué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.
  • 404 vs 403 cho resource của tenant khác: trả 403 là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 + Location khi tạo · 202 cho job dài (trả status URL) · 204 không body · 304 Not Modified.
  • 400 malformed · 401 chưa xác thực (kèm WWW-Authenticate) · 403 đã xác thực nhưng không có quyền · 404 · 409 conflict state · 412 precondition failed · 422 validation · 429 rate 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.

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

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" }
  ]
}
-- 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);
});