Skip to content

9.1 - REST API Design Principles ​

Tóm tắt: REST không phải "dùng JSON qua HTTP" mà là sáu ràng buộc kiến trúc, trong đó stateless là cái đắt giá nhất: không có trạng thái trên server nghĩa là bất kỳ instance nào cũng xử lý được bất kỳ request nào — đó chính là thứ cho phép scale ngang. Khái niệm thực dụng nhất trong bài là idempotency: gửi lại cùng một request phải cho cùng một kết quả. Nó quan trọng vì mạng sẽ hỏng, client sẽ retry, và một POST /orders bị gửi hai lần vì timeout sẽ tạo hai đơn hàng nếu bạn không thiết kế cho điều đó.

Mục tiêu bài học ​

Sau bài này bạn có thể:

  • Liệt kê sáu ràng buộc REST và nêu hệ quả thực tế của từng cái.
  • Phân biệt safe và idempotent, và biết method nào thuộc loại nào.
  • Chọn đúng giữa PUT và PATCH.
  • Thiết kế URL tài nguyên không chứa động từ.
  • Làm cho POST idempotent bằng idempotency key.

Nội dung bài học ​

9.1.1 — Sáu ràng buộc ​

Ràng buộcHệ quả thực tế
Client–ServerHai bên tiến hoá độc lập; đổi UI không cần đổi API
StatelessMỗi request tự chứa đủ thông tin → scale ngang được
CacheableResponse nói rõ nó cache được bao lâu → giảm tải
Uniform interfaceCùng một cách dùng cho mọi tài nguyên → không phải học lại
Layered systemClient không biết có proxy, cache hay gateway ở giữa
Code on demand (tuỳ chọn)Server gửi được code thực thi; hiếm dùng

Stateless là ràng buộc quan trọng nhất và cũng hay bị vi phạm nhất. Nó có nghĩa: server không nhớ gì giữa hai request. Không session trong bộ nhớ, không "bước hiện tại của wizard", không giỏ hàng lưu ở tiến trình.

Vi phạm nó thì scale ngang gãy ngay: request đầu vào pod A, request sau vào pod B, và pod B không biết gì. Người ta hay vá bằng sticky session — nhưng như vậy pod chết là mất trạng thái, và tải phân bổ không đều.

Trạng thái phải nằm ở client (token JWT) hoặc ở kho dùng chung (Redis, database).

9.1.2 — URL định danh tài nguyên ​

# ĐÚNG — danh từ số nhiều, động từ nằm ở HTTP method
GET    /api/v1/customers
GET    /api/v1/customers/{id}
POST   /api/v1/customers
PUT    /api/v1/customers/{id}
PATCH  /api/v1/customers/{id}
DELETE /api/v1/customers/{id}
GET    /api/v1/customers/{id}/contacts

# SAI — động từ trong URL
POST   /api/v1/getCustomers
POST   /api/v1/deleteCustomer
POST   /api/v1/customers/create

Quy ước: danh từ số nhiều, chữ thường, phân tách bằng dấu gạch ngang. /purchase-orders chứ không phải /purchaseOrders hay /purchase_orders.

Còn những hành động không phải CRUD? Ví dụ "duyệt đơn hàng", "gửi lại email xác nhận". Có hai cách chấp nhận được:

# 1. Coi hành động như một tài nguyên con
POST /api/v1/orders/{id}/approval
POST /api/v1/orders/{id}/cancellation

# 2. Đổi trạng thái bằng PATCH
PATCH /api/v1/orders/{id}   {"status": "approved"}

Cách 1 rõ hơn khi hành động có tác dụng phụ (gửi email, trừ kho) hoặc cần tham số riêng. Đừng cố ép mọi thứ vào CRUD — một POST /orders/{id}/approval dễ hiểu hơn nhiều so với một PATCH mà server phải đoán ý.

9.1.3 — Safe và idempotent ​

MethodSafeIdempotentNghĩa
GETCóCóKhông đổi gì
HEADCóCóNhư GET nhưng không có body
OPTIONSCóCóHỏi khả năng
PUTKhôngCóThay thế toàn bộ
DELETEKhôngCóXoá
POSTKhôngKhôngTạo mới
PATCHKhôngKhông bắt buộcSửa một phần
  • Safe = không thay đổi trạng thái. Crawler và prefetch của trình duyệt tự do gọi.
  • Idempotent = gọi một lần hay mười lần thì trạng thái cuối giống nhau.

Đây không phải lý thuyết suông. Mạng hỏng, client timeout rồi retry, và bạn không biết request trước đã tới server hay chưa:

Client --POST /orders--> [timeout]
                          ^ request ĐÃ tới server, đơn hàng ĐÃ tạo,
                            chỉ có response bị mất
Client --POST /orders--> tạo thêm một đơn hàng nữa

Hệ quả thiết kế:

  • DELETE phải idempotent: xoá một thứ đã xoá vẫn trả 204, không phải 404. Client retry không nhận lỗi giả.
  • PUT idempotent tự nhiên vì nó thay toàn bộ: gửi cùng body mười lần cho cùng kết quả.
  • PATCH thường không idempotent: {"credit": "+100"} gửi hai lần là cộng 200. Thiết kế PATCH theo kiểu đặt giá trị ({"credit": 500}) thay vì thay đổi tương đối thì nó trở thành idempotent.

9.1.4 — Làm POST idempotent ​

POST không idempotent theo bản chất, nhưng bạn làm cho nó idempotent được:

http
POST /api/v1/orders
Idempotency-Key: 7f3a9c2e-1b4d-4a8f-9e12-5c6d7e8f9a0b

{ "customerId": "...", "items": [...] }

Server lưu khoá đó cùng kết quả:

csharp
public async Task<Result<OrderDto>> CreateAsync(
    CreateOrderRequest request, string idempotencyKey, CancellationToken ct)
{
    var existing = await _store.GetAsync(idempotencyKey, ct);
    if (existing is not null)
        return Result.Success(existing.Response);    // trả lại kết quả cũ

    var order = await _orders.CreateAsync(request, ct);
    await _store.SaveAsync(idempotencyKey, order, TimeSpan.FromHours(24), ct);
    return Result.Success(order);
}

Client sinh khoá một lần rồi dùng lại cho mọi lần retry của cùng thao tác. Kết quả: retry an toàn, không tạo đơn trùng.

Đây là mẫu bắt buộc cho mọi API có liên quan tới tiền — Stripe và các cổng thanh toán đều làm vậy.

9.1.5 — PUT hay PATCH ​

http
# PUT — thay thế TOÀN BỘ. Trường không gửi sẽ bị XOÁ.
PUT /api/v1/customers/{id}
{ "name": "Nguyen Van A", "email": "[email protected]", "phone": null }

# PATCH — chỉ sửa trường được gửi
PATCH /api/v1/customers/{id}
{ "phone": "0901234567" }

Lỗi hay gặp: cài PUT nhưng xử lý như PATCH — chỉ cập nhật trường có trong body. Client gửi PUT thiếu một trường sẽ tưởng trường đó bị xoá; nó không bị xoá, và dữ liệu lệch nhau mà không ai biết.

Hoặc cài đúng PUT mà client dùng sai: gửi PUT chỉ với phone và mất tên cùng email.

Lời khuyên thực dụng: cài PATCH cho cập nhật một phần và đừng hỗ trợ PUT trừ khi bạn thật sự cần ngữ nghĩa thay thế toàn bộ. Hai endpoint trông giống nhau nhưng khác ngữ nghĩa là nguồn lỗi bền vững.

Với PATCH, có hai chuẩn: JSON Merge Patch (RFC 7386, đơn giản — gửi object với các trường cần đổi) và JSON Patch (RFC 6902, mạnh hơn — danh sách thao tác add/remove/replace). Merge Patch đủ cho 95% trường hợp.

9.1.6 — Mô hình trưởng thành Richardson ​

MứcĐặc điểmThực tế
0Một endpoint, mọi thứ qua POSTSOAP trá hình
1Nhiều tài nguyên, vẫn chỉ POSTNửa vời
2Tài nguyên + HTTP method + status code đúngPhần lớn API ở đây
3Thêm HATEOAS — response chứa linkHiếm

Mức 2 là mục tiêu thực tế. Mức 3 (HATEOAS) nghe hay nhưng hiếm khi đáng: client vẫn hardcode URL, và chi phí duy trì link không được bù lại.

Ngoại lệ đáng làm là link phân trang, vì client thật sự dùng chúng:

json
{
  "items": [...],
  "page": 2,
  "pageSize": 20,
  "totalCount": 342,
  "_links": {
    "self": "/api/v1/customers?page=2&pageSize=20",
    "next": "/api/v1/customers?page=3&pageSize=20",
    "prev": "/api/v1/customers?page=1&pageSize=20"
  }
}

Chi tiết ở bài 9.4.

9.1.7 — Rà lại code của bạn ​

Danh sách rà soát thiết kế REST

  • Không có động từ nào trong đường dẫn tài nguyên.
  • Tài nguyên dùng danh từ số nhiều, chữ thường, phân tách bằng gạch ngang.
  • Không có trạng thái nào lưu trong bộ nhớ tiến trình giữa hai request.
  • DELETE trả 204 kể cả khi tài nguyên đã bị xoá trước đó.
  • PATCH thiết kế theo kiểu đặt giá trị, không phải thay đổi tương đối.
  • Không hỗ trợ cả PUT lẫn PATCH cho cùng một tài nguyên trừ khi ngữ nghĩa thật sự khác.
  • Mọi endpoint tạo tài nguyên có liên quan tới tiền đều hỗ trợ Idempotency-Key.
  • Hành động không phải CRUD được mô hình hoá thành tài nguyên con.
  • Response danh sách có link phân trang.

Bài tập áp dụng ​

Bài 1 — Tạo đơn trùng do thử lại ​

Viết endpoint POST /orders không có khoá idempotency, mô phỏng client hết giờ chờ rồi thử lại, và xác nhận có hai đơn hàng. Thêm Idempotency-Key và kiểm chứng lại.

Tiêu chí hoàn thành: bạn nêu được vì sao client không thể tự tránh việc này.

Gợi ý và lời giải — Bài 1

Gợi ý. Khi client hết giờ chờ, nó biết gì về kết quả? Câu trả lời: không biết gì cả. Request có thể đã tới server và được xử lý xong, hoặc chưa bao giờ tới.

Lời giải — tái hiện:

csharp
app.MapPost("/orders", async (CreateOrderRequest req, CrmDbContext db, CancellationToken ct) =>
{
    await Task.Delay(3000, ct);                  // mô phỏng xử lý chậm
    var order = new Order { CustomerId = req.CustomerId, Total = req.Total };
    db.Orders.Add(order);
    await db.SaveChangesAsync(ct);
    return Results.Created($"/orders/{order.Id}", order);
});
bash
# Client hết giờ after 2 giây rồi thử lại
curl -m 2 -X POST localhost:5000/orders -d '{"customerId":1,"total":500000}' -H "Content-Type: application/json"
# curl: (28) Operation timed out
curl -m 10 -X POST localhost:5000/orders -d '{"customerId":1,"total":500000}' -H "Content-Type: application/json"
# {"id":2,...}

psql -c 'SELECT COUNT(*) FROM "Orders" WHERE "CustomerId" = 1;'
# 2     <- HAI đơn hàng cho một lần đặt

Vì sao client không thể tự tránh. Khi hết giờ chờ, client ở trong tình trạng hoàn toàn mù:

Khả năngClient phân biệt được?
Request chưa tới serverKhông
Request tới, server đang xử lýKhông
Server xử lý xong, phản hồi mất trên đường vềKhông

Cả ba trường hợp trông giống hệt nhau từ phía client. Nếu client không thử lại, nó có thể mất một đơn hàng hợp lệ. Nếu có thử lại, nó có thể tạo đơn trùng. Không có lựa chọn an toàn nào — vấn đề phải được giải quyết ở phía server.

Bản có khoá idempotency:

csharp
app.MapPost("/orders", async (
    CreateOrderRequest req,
    [FromHeader(Name = "Idempotency-Key")] string? key,
    CrmDbContext db, CancellationToken ct) =>
{
    if (string.IsNullOrWhiteSpace(key))
        return Results.BadRequest("Thiếu header Idempotency-Key");

    // Ghi khoá TRƯỚC, dựa vào ràng buộc duy nhất của database
    var record = new IdempotencyRecord { Key = key, CreatedAt = DateTime.UtcNow };
    db.IdempotencyRecords.Add(record);
    try
    {
        await db.SaveChangesAsync(ct);
    }
    catch (DbUpdateException) when (IsUniqueViolation())
    {
        // Đã xử lý rồi — trả lại kết quả cũ
        var saved = await db.IdempotencyRecords.AsNoTracking()
                           .FirstAsync(x => x.Key == key, ct);
        return Results.Ok(JsonSerializer.Deserialize<Order>(saved.Response!));
    }

    var order = new Order { CustomerId = req.CustomerId, Total = req.Total };
    db.Orders.Add(order);
    record.Response = JsonSerializer.Serialize(order);
    await db.SaveChangesAsync(ct);

    return Results.Created($"/orders/{order.Id}", order);
});

Thử lại với cùng khoá cho ra cùng một đơn hàng, và bảng Orders chỉ có một dòng.

Ba điểm quan trọng trong cách cài đặt trên:

  1. Dựa vào ràng buộc duy nhất của database, không dựa vào việc kiểm tra trước. Kiểm tra rồi mới ghi có khoảng trống đua, đúng như bài 2.6 đã chứng minh. Ràng buộc duy nhất là lớp bảo đảm thật.
  2. Lưu lại phản hồi, để lần thử lại nhận được cùng một kết quả chứ không phải một lỗi.
  3. Dọn bản ghi cũ theo lịch — thường giữ 24 tới 48 giờ là đủ.

Client sinh khoá thế nào. Một Guid cho mỗi ý định thao tác, không phải mỗi lần gọi:

ts
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) {
  try { return await post('/orders', body, { 'Idempotency-Key': key }); }   // CÙNG key
  catch (e) { if (i === 2) throw e; await sleep(2 ** i * 1000); }
}

Sinh khoá mới ở mỗi lần thử lại là quay lại đúng vấn đề ban đầu.

Endpoint nào cần. Mọi endpoint thay đổi dữ liệu và không idempotent tự nhiên: tạo bản ghi, thanh toán, gửi thông báo. PUT và DELETE idempotent theo bản chất nên không cần. Module 17 mở rộng ý này cho xử lý thông điệp.

Bài 2 — PUT đúng ngữ nghĩa thay thế toàn bộ ​

Cài PUT đúng nghĩa thay thế toàn bộ, gọi với thân request thiếu trường email, và ghi lại điều xảy ra. Thảo luận xem client của bạn có biết điều đó không.

Tiêu chí hoàn thành: bạn nêu được vì sao PUT đúng chuẩn lại nguy hiểm với client viết cẩu thả.

Gợi ý và lời giải — Bài 2

Gợi ý. Ngữ nghĩa của PUT là "thay thế tài nguyên bằng cái tôi gửi". Vậy trường không được gửi nghĩa là gì?

Lời giải.

csharp
app.MapPut("/customers/{id:int}", async (int id, CustomerDto dto, CrmDbContext db, CancellationToken ct) =>
{
    var customer = await db.Customers.FindAsync([id], ct);
    if (customer is null) return Results.NotFound();

    // PUT = THAY THẾ TOÀN BỘ
    customer.Name  = dto.Name;
    customer.Email = dto.Email;        // dto.Email là null vì client không gửi
    customer.Phone = dto.Phone;
    customer.Note  = dto.Note;

    await db.SaveChangesAsync(ct);
    return Results.NoContent();
});
bash
curl -X PUT localhost:5000/customers/42 \
     -H "Content-Type: application/json" \
     -d '{"name":"Nguyễn Văn An"}'

Kết quả: Email, Phone, Note đều bị xoá thành null. Theo chuẩn HTTP, đây là hành vi đúng — client đã nói "tài nguyên này giờ chỉ có tên".

Vì sao nguy hiểm. Client thường không nghĩ như vậy. Mẫu sai rất phổ biến ở phía giao diện:

ts
// Form chỉ cho sửa tên
await fetch(`/customers/${id}`, {
  method: 'PUT',
  body: JSON.stringify({ name: tenMoi })      // chỉ gửi trường vừa sửa
});

Lập trình viên frontend nghĩ "tôi chỉ sửa tên nên chỉ gửi tên". Server làm đúng chuẩn và xoá sạch phần còn lại. Không bên nào sai về mặt kỹ thuật, nhưng dữ liệu đã mất.

Tệ hơn: lỗi này không báo gì cả. Phản hồi là 204 No Content — thành công. Người dùng chỉ phát hiện khi mở lại hồ sơ và thấy email biến mất.

Ba cách xử lý:

csharp
// Cách 1 — dùng PATCH cho cập nhật một phần (khuyến nghị)
app.MapPatch("/customers/{id:int}", async (int id, JsonElement body, ...) =>
{
    if (body.TryGetProperty("name", out var n))  customer.Name = n.GetString();
    if (body.TryGetProperty("email", out var e)) customer.Email = e.GetString();
    // trường không gửi -> không đụng tới
});

// Cách 2 — PUT nhưng bắt buộc gửi đủ, từ chối nếu thiếu
public record CustomerDto
{
    public required string Name { get; init; }
    public required string Email { get; init; }
    public required string Phone { get; init; }
}
// thiếu trường -> 400 với thông báo rõ ràng

// Cách 3 — JSON Patch theo RFC 6902
[{ "op": "replace", "path": "/name", "value": "Nguyễn Văn An" }]

Cách 2 đáng chú ý. Nó giữ đúng ngữ nghĩa PUT mà vẫn an toàn: client quên gửi trường nào sẽ nhận 400 kèm danh sách trường thiếu, thay vì âm thầm mất dữ liệu. Từ .NET 8, required kết hợp với System.Text.Json làm việc này tự động — xem bài 5.6.

Bảng so sánh:

PUTPATCH
Ngữ nghĩaThay thế toàn bộCập nhật một phần
Trường không gửiBị xoáGiữ nguyên
IdempotentCóTuỳ cài đặt
Phân biệt "xoá" và "không đổi"Không cầnCần — xem bài 2.5

Khuyến nghị thực dụng. Với API phục vụ form có nhiều trường, dùng PATCH. Chỉ dùng PUT khi client thật sự gửi toàn bộ trạng thái — ví dụ đồng bộ dữ liệu giữa hai hệ thống, hoặc cấu hình được quản lý bằng file.

Và dù chọn cách nào, ghi rõ trong tài liệu API. Đây là một trong những chỗ mà giả định ngầm giữa hai đội gây mất dữ liệu nhiều nhất.

Bài 3 — Phá tính phi trạng thái ​

Lưu bước hiện tại của một biểu mẫu nhiều bước vào bộ nhớ tiến trình, chạy hai thể hiện sau bộ cân bằng tải luân phiên, và ghi lại tỉ lệ request thất bại.

Tiêu chí hoàn thành: bạn tính được tỉ lệ thất bại theo công thức, và nêu được ba nơi đúng để lưu trạng thái đó.

Gợi ý và lời giải — Bài 3

Gợi ý. Bài này lặp lại thí nghiệm ở bài 2.8 nhưng với hệ quả nghiệp vụ rõ hơn. Công thức tỉ lệ thất bại: với n thể hiện và phân phối đều, xác suất một request rơi đúng thể hiện giữ trạng thái là 1/n.

Lời giải.

csharp
public static class WizardStore
{
    private static readonly ConcurrentDictionary<string, WizardState> _store = new();
    public static void Save(string id, WizardState s) => _store[id] = s;
    public static WizardState? Get(string id) => _store.GetValueOrDefault(id);
}

app.MapPost("/wizard/{id}/steps/{n:int}", (string id, int n, StepData data) =>
{
    var s = WizardStore.Get(id) ?? new WizardState();
    s.Step = n;
    s.Data[n] = data;
    WizardStore.Save(id, s);
    return Results.Ok(new { nextStep = n + 1 });
});

app.MapGet("/wizard/{id}", (string id) =>
    WizardStore.Get(id) is { } s ? Results.Ok(s) : Results.NotFound("Phiên đã hết hạn"));

Chạy hai thể hiện sau Nginx luân phiên, đi qua 5 bước:

text
Bước 1 -> thể hiện A   (lưu trạng thái ở A)
Bước 2 -> thể hiện B   -> KHÔNG thấy trạng thái -> tạo mới, MẤT dữ liệu bước 1
Bước 3 -> thể hiện A   -> thấy trạng thái cũ của A, thiếu bước 2
Bước 4 -> thể hiện B
Bước 5 -> thể hiện A   -> dữ liệu chắp vá, không hoàn tất được

Tỉ lệ thất bại theo công thức. Với n thể hiện, xác suất một request rơi đúng thể hiện giữ trạng thái là 1/n, nên tỉ lệ thất bại là (n−1)/n:

Số thể hiệnTỉ lệ request đọc sai
250%
367%
580%

Với biểu mẫu 5 bước trên 2 thể hiện, xác suất hoàn tất trọn vẹn chỉ là (1/2)⁴ ≈ 6%.

Vì sao lỗi này thoát khỏi mọi khâu kiểm tra. Trên máy phát triển có một thể hiện, tỉ lệ thất bại là 0%. Nó chạy hoàn hảo. Lỗi chỉ xuất hiện sau khi mở rộng lên hai thể hiện — thường là lúc hệ thống đã có người dùng thật.

Ba nơi đúng để lưu trạng thái này:

NơiHợp khiLưu ý
Client — trong biểu mẫu hoặc sessionStorageDữ liệu không nhạy cảm, không cần xác thực giữa các bướcServer phải xác thực lại toàn bộ ở bước cuối
Redis với thời hạnCần giữ giữa các thiết bị, hoặc dữ liệu lớnĐặt thời hạn, đừng để tích tụ
Database dưới dạng bản nhápNgười dùng cần quay lại sau nhiều ngàyCó bản ghi thật, truy vấn được, báo cáo được

Lựa chọn tốt nhất cho phần lớn trường hợp là cách thứ nhất. Nếu client giữ trạng thái và gửi toàn bộ ở bước cuối, server hoàn toàn phi trạng thái — và bạn không cần thêm hạ tầng nào.

Điều bắt buộc: server không được tin dữ liệu client gửi. Mọi quy tắc nghiệp vụ phải được kiểm tra lại ở bước cuối, đúng nguyên tắc ở bài 2.3.

Vì sao "dính phiên" không phải giải pháp. Cấu hình bộ cân bằng tải luôn gửi một người dùng về cùng một thể hiện sẽ làm biểu mẫu chạy được. Nhưng nó giữ nguyên bệnh:

  • Thể hiện đó khởi động lại là mất trạng thái — và triển khai nào cũng khởi động lại.
  • Tải phân bố không đều.
  • Không thu nhỏ số thể hiện được khi vắng khách.

Danh sách trạng thái cục bộ cần rà trong dự án:

bash
grep -rn "static.*Dictionary\|static.*List\|static.*ConcurrentDictionary" --include=*.cs src/ \
  | grep -v "readonly.*=.*new.*{.*}"      # bỏ bảng tra hằng số

Mỗi kết quả là một ứng viên: nó có bị ghi vào sau khi khởi tạo không? Có thì nó là trạng thái cục bộ.

Tự kiểm tra ​

Vì sao stateless là ràng buộc quan trọng nhất của REST?

Vì nó cho phép scale ngang: mỗi request tự chứa đủ thông tin nên bất kỳ instance nào cũng xử lý được bất kỳ request nào. Vi phạm nó thì request đầu vào một pod và request sau vào pod khác sẽ hỏng. Sticky session chỉ là cách vá, vì pod chết là mất trạng thái và tải phân bổ không đều.

Safe và idempotent khác nhau thế nào?

Safe nghĩa là không thay đổi trạng thái, ví dụ GET và HEAD. Idempotent nghĩa là gọi một lần hay mười lần thì trạng thái cuối giống nhau, ví dụ PUT và DELETE. Mọi method safe đều idempotent nhưng ngược lại thì không: PUT thay đổi dữ liệu nhưng vẫn idempotent.

Vì sao idempotency quan trọng trong thực tế?

Vì mạng sẽ hỏng và client sẽ retry, mà client không biết request trước đã tới server hay chưa. Một POST tạo đơn hàng bị timeout rồi retry sẽ tạo hai đơn nếu bạn không thiết kế cho điều đó. Vì vậy DELETE phải trả 204 kể cả khi đã xoá, và POST tạo giao dịch tiền phải có idempotency key.

Làm sao để POST idempotent?

Dùng header Idempotency-Key: client sinh một khoá duy nhất cho thao tác và dùng lại nó cho mọi lần retry. Server lưu khoá cùng kết quả trong một khoảng thời gian, và khi thấy khoá đã tồn tại thì trả lại kết quả cũ thay vì tạo mới. Đây là mẫu bắt buộc cho mọi API liên quan tới tiền.

Nên dùng PUT hay PATCH?

PUT thay thế toàn bộ nên trường không gửi sẽ bị xoá; PATCH chỉ sửa trường được gửi. Lời khuyên thực dụng là cài PATCH cho cập nhật một phần và không hỗ trợ PUT trừ khi thật sự cần ngữ nghĩa thay thế toàn bộ, vì hai endpoint trông giống nhau mà khác ngữ nghĩa là nguồn lỗi bền vững.

Có nên làm HATEOAS không?

Phần lớn API dừng ở mức 2 của mô hình Richardson, tức tài nguyên cộng HTTP method cộng status code đúng, và đó là mục tiêu thực tế. HATEOAS đầy đủ hiếm khi đáng vì client vẫn hardcode URL. Ngoại lệ đáng làm là link phân trang, vì client thật sự dùng chúng.

Kết luận ​

Ba điều đáng nhớ nhất:

  1. Stateless là thứ cho phép scale ngang. Trạng thái nằm ở client hoặc kho dùng chung.
  2. Idempotency không phải lý thuyết — nó là cách API sống sót qua mạng không ổn định.
  3. Mức 2 Richardson là đủ. Đừng đuổi theo HATEOAS, trừ link phân trang.

Tham khảo ​

Điều hướng ​

Phát hành theo giấy phép MIT.