Skip to content

2.5 - API và JSON ​

Tóm tắt: JSON đơn giản tới mức người ta quên rằng nó có giới hạn thật. Cái bẫy tốn kém nhất: JavaScript lưu mọi số bằng double, nên số nguyên vượt quá 9.007.199.254.740.991 bị làm tròn — một Id kiểu long hay một Snowflake ID sẽ sai ở vài chữ số cuối mà không có lỗi nào được ném ra. Bài này đi qua nguyên tắc REST dùng được trong thực tế, cách thiết kế response không phá vỡ client cũ, chuẩn ProblemDetails cho lỗi, bốn thứ JSON không có sẵn (số nguyên lớn, ngày tháng, decimal chính xác, dữ liệu nhị phân), và chuyện trường vắng mặt khác trường bằng null.

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

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

  • Thiết kế đường dẫn và method REST theo tài nguyên thay vì theo hành động.
  • Giải thích vì sao Id kiểu long phải trả về dưới dạng chuỗi trong JSON.
  • Trả lỗi theo chuẩn ProblemDetails thay vì tự bịa định dạng.
  • Chọn đúng cách biểu diễn ngày tháng và số tiền trong JSON.
  • Phân biệt trường vắng mặt với trường có giá trị null.

Cần biết trước: 2.4 — HTTP/HTTPS (method, status code) và bài 1.1 (vì sao double sai số). Bài 1 dùng API CrmLab ở mục 2.3.6.

Nội dung bài học ​

2.5.1 — REST: đặt tên theo tài nguyên, không theo hành động ​

❌ POST /api/getCustomerById          ✅ GET    /api/customers/42
❌ POST /api/createCustomer           ✅ POST   /api/customers
❌ POST /api/deleteCustomer?id=42     ✅ DELETE /api/customers/42
❌ GET  /api/updateCustomerName       ✅ PATCH  /api/customers/42

Nguyên tắc: đường dẫn là danh từ, method là động từ. Đường dẫn chỉ ra cái gì, method chỉ ra làm gì với nó.

Vài quy ước đi kèm, dùng thẳng được:

GET    /api/customers?page=2&pageSize=50&sort=-createdAt   # danh sách, phân trang, sắp xếp
GET    /api/customers/42/orders                            # tài nguyên con
POST   /api/customers/42/orders                            # tạo đơn cho khách 42
POST   /api/orders/99/cancel                               # hành động không map được vào CRUD

Dòng cuối là một ngoại lệ hợp lý. Không phải thao tác nghiệp vụ nào cũng ép được vào bốn động từ; "huỷ đơn" là một hành động có quy tắc riêng, và POST /orders/99/cancel rõ ràng hơn nhiều so với cố nhét vào PATCH.

2.5.2 — JSON: bốn thứ nó không có ​

JSON chỉ có sáu kiểu: chuỗi, số, boolean, null, mảng, object. Không có ngày tháng. Không có decimal. Không có số nguyên lớn. Không có kiểu nhị phân.

1. Số nguyên lớn — cái bẫy nguy hiểm nhất

json
{ "id": 9007199254740993 }

JavaScript lưu mọi số bằng double 64-bit, chỉ giữ chính xác được số nguyên tới 2⁵³ − 1 = 9.007.199.254.740.991. Vượt qua đó:

javascript
JSON.parse('{"id": 9007199254740993}').id   // 9007199254740992  ← sai 1 đơn vị

Không có ngoại lệ nào được ném. Dữ liệu chỉ lặng lẽ sai. Với Id kiểu long, Snowflake ID hoặc mã giao dịch, hậu quả là client thao tác nhầm bản ghi.

Cách xử lý: trả số lớn dưới dạng chuỗi. System.Text.Json (thư viện JSON có sẵn của .NET) làm được việc này bằng một thuộc tính đặt trên đúng trường cần đổi:

csharp
using System.Text.Json;
using System.Text.Json.Serialization;

var dto = new CustomerDto { Id = 9007199254740993, TotalOrders = 12 };
Console.WriteLine(JsonSerializer.Serialize(dto));

public class CustomerDto
{
    [JsonNumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)]
    public long Id { get; set; }
    public int TotalOrders { get; set; }
}

Kết quả chạy thật:

text
{"Id":"9007199254740993","TotalOrders":12}

Id thành chuỗi, TotalOrders vẫn là số. AllowReadingFromString cho phép client gửi Id lên dưới dạng chuỗi mà server vẫn đọc được.

2. Ngày tháng — JSON không có kiểu ngày, nên phải quy ước. Dùng ISO 8601 với múi giờ:

json
{ "createdAt": "2026-09-24T10:30:00+07:00" }   ✅ không mơ hồ
{ "createdAt": "2026-09-24 10:30:00" }         ❌ múi giờ nào?
{ "createdAt": 1758697800 }                    ⚠️ giây hay mili giây?

3. Số tiền — double của JSON gặp đúng vấn đề đã nói ở bài 1.1. An toàn nhất là dùng chuỗi, hoặc số nguyên tính theo đơn vị nhỏ nhất của loại tiền đó, luôn kèm mã tiền tệ:

json
{ "amount": 1500000, "currency": "VND" }
{ "amount": "1500000.50", "currency": "USD" }
{ "amountInMinorUnits": 150000050, "currency": "USD" }

Đồng Việt Nam không có đơn vị nhỏ hơn đồng, nên số tiền VND là số nguyên. USD có cent, nên 150000050 cent nghĩa là 1.500.000,50 USD.

4. Nhị phân — không có. Phải mã hoá Base64, và chấp nhận phình thêm khoảng 33%.

2.5.3 — Vắng mặt khác null ​

json
{ "name": "A", "phone": null }    // "hãy XOÁ số điện thoại"
{ "name": "A" }                   // "đừng đụng vào số điện thoại"

Với PATCH, hai thứ này phải được đối xử khác nhau. Trong .NET, phân biệt bằng JsonElement hoặc một kiểu bọc tự định nghĩa — nếu chỉ dùng string? Phone thì cả hai trường hợp đều ra null và bạn mất thông tin.

Đây là lý do nhiều API chọn PUT (thay thế toàn bộ) thay vì PATCH: nó tránh hẳn sự mơ hồ này.

2.5.4 — Lỗi: dùng chuẩn có sẵn ​

Đừng tự bịa định dạng lỗi. RFC 7807 / 9457 đã chuẩn hoá ProblemDetails, và ASP.NET Core hỗ trợ sẵn:

json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10",
  "title": "Email đã tồn tại",
  "status": 409,
  "detail": "Địa chỉ [email protected] đã được đăng ký cho một khách hàng khác.",
  "instance": "/api/customers"
}
csharp
// ASP.NET Core sinh sẵn định dạng này (trong minimal API dùng Results.Problem)
return Problem(
    title: "Email đã tồn tại",
    detail: "Địa chỉ [email protected] đã được đăng ký cho một khách hàng khác.",
    statusCode: StatusCodes.Status409Conflict);

Lợi ích không nằm ở cái tên: client dùng một cách xử lý lỗi cho mọi endpoint, và công cụ bên ngoài hiểu được mà không cần đọc tài liệu riêng của bạn.

Với lỗi kiểm tra dữ liệu (thiếu trường, sai định dạng), ASP.NET Core trả 400 kèm ValidationProblemDetails: vẫn định dạng trên, thêm trường errors liệt kê lỗi theo từng trường. Quy ước mã trạng thái của khoá học nằm ở bài 2.4.

2.5.5 — Response ổn định: bọc hay không bọc ​

json
// Kiểu bọc
{ "data": { "id": 42 }, "meta": { "requestId": "abc" } }

// Kiểu trần
{ "id": 42 }

Cả hai đều dùng được, miễn nhất quán trong toàn bộ API. Nhưng với danh sách thì kiểu bọc có lợi rõ ràng, vì phải chở thêm thông tin phân trang:

json
{
  "items": [ ... ],
  "page": 2,
  "pageSize": 50,
  "totalItems": 3000000,
  "totalPages": 60000
}

Lưu ý hiệu năng: totalItems trên bảng lớn đòi một câu COUNT(*) quét rất nhiều dòng. Với dữ liệu hàng triệu bản ghi, cân nhắc phân trang theo con trỏ (cursor) thay vì theo số trang — chi tiết ở bài hiệu năng truy vấn.

2.5.6 — System.Text.Json trong .NET ​

csharp
builder.Services.ConfigureHttpJsonOptions(o =>
{
    o.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    o.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});

Ba điều nên biết:

  • System.Text.Json phân biệt hoa thường theo mặc định, khác với Newtonsoft.Json. ASP.NET Core tự dùng bộ tuỳ chọn JsonSerializerOptions.Web (không phân biệt hoa thường, tên trường kiểu camelCase), nhưng khi tự gọi JsonSerializer trong ứng dụng console thì không: JSON có "name" sẽ không khớp thuộc tính Name. Truyền JsonSerializerOptions.Web vào là hết lỗi (bài 2.13 có ví dụ chạy thật).
  • Nó không hỗ trợ tham chiếu vòng nếu chưa bật ReferenceHandler.IgnoreCycles. Trả thẳng entity của EF Core có quan hệ hai chiều là gặp ngay.
  • Đừng trả entity trực tiếp. Dùng DTO — vừa tránh vòng lặp, vừa không vô tình phơi ra cột nội bộ như PasswordHash.

Tài liệu: System.Text.Json overview.

2.5.7 — Danh sách rà soát cho dự án thật ​

Danh sách rà soát API và JSON

  • Đường dẫn là danh từ số nhiều; hành động nằm ở HTTP method.
  • Id kiểu long và mọi số vượt 2^53 đều trả về dưới dạng chuỗi.
  • Ngày tháng theo ISO 8601 kèm múi giờ, không dùng chuỗi tự chế.
  • Số tiền trả về dạng chuỗi hoặc số nguyên đơn vị nhỏ nhất, kèm mã tiền tệ.
  • Lỗi trả theo ProblemDetails, thống nhất trên mọi endpoint.
  • Không trả entity của EF Core trực tiếp; luôn đi qua DTO.
  • Endpoint danh sách có phân trang, và đã cân nhắc cursor cho bảng lớn.

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

Bài 1 — Tự chứng minh mất chính xác số nguyên lớn ​

Mở console trình duyệt (F12, tab Console), chạy JSON.parse('{"id":9007199254740993}').id. Ghi lại kết quả. Sau đó đăng nhập CrmLab bằng tài khoản [email protected], gọi GET /api/customers và xem client JavaScript nhận được id nào cho khách hàng thứ tư.

Tiêu chí hoàn thành: bạn nêu được ngưỡng chính xác là bao nhiêu, chỉ ra được id bị sai trong CrmLab, và nêu cách sửa ở phía server.

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

Gợi ý. JSON không có kiểu số nguyên. Đặc tả chỉ có một kiểu number duy nhất, và JavaScript hiện thực nó bằng số thực dấu phẩy động 64 bit. Hãy tìm xem kiểu đó biểu diễn chính xác được số nguyên tới đâu.

Lời giải — chạy trong console trình duyệt hoặc Node:

js
JSON.parse('{"id":9007199254740993}').id    // 9007199254740992
JSON.parse('{"id":9223372036854775807}').id // 9223372036854776000
Number.MAX_SAFE_INTEGER                     // 9007199254740991

Kết quả chạy thật (Node 22):

Giá trị gửi điJavaScript nhận đượcSai lệch
900719925474099390071992547409921
9223372036854775807 (long.MaxValue)9223372036854776000193

Với CrmLab. Lấy token của Admin (bước 4 ở mục 2.3.6, đổi email thành [email protected]) rồi xem JSON thô:

bash
curl -s http://localhost:5000/api/customers -H "Authorization: Bearer $TOKEN"
text
[{"id":101,"name":"Cong ty Anh Duong","ownerId":1},{"id":102,"name":"Cua hang Minh Phat","ownerId":1},{"id":103,"name":"Nha sach Hoa Mai","ownerId":2},{"id":9007199254740993,"name":"Khach co ma lon","ownerId":2}]

Server gửi đúng 9007199254740993. Giờ để JavaScript đọc cùng phản hồi đó. Cách dễ nhất là Node (có sẵn fetch từ bản 18):

bash
TOKEN=$TOKEN node -e 'fetch("http://localhost:5000/api/customers",
  { headers: { Authorization: "Bearer " + process.env.TOKEN } })
  .then(r => r.json()).then(a => console.log(a.map(c => c.id)))'
text
[ 101, 102, 103, 9007199254740992 ]

Không có lỗi nào, chỉ có một chữ số cuối bị đổi. Client cầm id sai đó gọi lại server:

bash
curl -s -o /dev/null -w "%{http_code}\n" \
  http://localhost:5000/api/customers/9007199254740992 -H "Authorization: Bearer $TOKEN"
text
404

Vì sao. Số thực 64 bit theo chuẩn IEEE 754 dành 53 bit cho phần định trị, nên nó biểu diễn chính xác mọi số nguyên trong khoảng từ -(2⁵³−1) tới 2⁵³−1, tức ±9007199254740991. Vượt ngưỡng đó, các số nguyên liền kề bắt đầu ánh xạ về cùng một giá trị biểu diễn được, và phần lẻ bị làm tròn.

Đây cùng một nguyên nhân với sai số double ở bài 1.1 — chỉ khác là ở đó nó xuất hiện khi cộng số thập phân, còn ở đây nó xuất hiện khi truyền số nguyên lớn.

Hậu quả thực tế. Kiểu long rất phổ biến làm khoá chính, và các hệ sinh mã phân tán như Snowflake tạo ra số vượt xa 2⁵³. Khi đó:

  • Client đọc được một id khác với id thật trong database.
  • Gọi lại bằng id đó trả về 404 như trên, hoặc tệ hơn là trả về nhầm bản ghi khác nếu id bị làm tròn trùng với một bản ghi có thật.
  • Lỗi không xuất hiện trong kiểm thử vì dữ liệu thử thường có id nhỏ.

Ba cách sửa, theo thứ tự nên dùng:

  1. Đánh dấu riêng trường id để ghi thành chuỗi, như mục 2.5.2:

    csharp
    [JsonNumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)]
    public long Id { get; set; }

    Có thể cấu hình cho toàn bộ API, nhưng cẩn thận tác dụng phụ:

    csharp
    builder.Services.ConfigureHttpJsonOptions(o =>
        o.SerializerOptions.NumberHandling = JsonNumberHandling.WriteAsString);

    Cấu hình toàn cục biến mọi số thành chuỗi, kể cả totalPages hay giá tiền. Chạy thật với đối tượng { Id, TotalOrders, Price }:

    text
    {"Id":"9007199254740993","TotalOrders":"12","Price":"1500000.5"}

    Và vì thiếu AllowReadingFromString, server lại từ chối chính kiểu dữ liệu nó vừa gửi đi khi client gửi lên {"Id":"42"}:

    text
    The JSON value could not be converted to System.Int64. Path: $.Id | LineNumber: 0 | BytePositionInLine: 10.

    Vì vậy nên đặt thuộc tính trên từng trường id thay vì cấu hình toàn cục.

  2. Dùng Guid làm khoá công khai. Vốn đã là chuỗi trong JSON nên không có vấn đề. Đánh đổi là chỉ mục lớn hơn và ghi chậm hơn — bài 12.1 phân tích đánh đổi này.

  3. Giữ id dưới ngưỡng an toàn. Chỉ khả thi khi bạn kiểm soát được cách sinh khoá và chắc chắn không dùng mã phân tán.

Điểm dễ bỏ sót. decimal trong C# giữ được 28 chữ số, nhưng qua JSON tới JavaScript nó cũng thành double. Chạy thật trong Node:

js
JSON.parse('{"a":12345678901234.56}').a     // 12345678901234.56   (16 chữ số: còn đúng)
JSON.parse('{"a":1234567890123456.78}').a   // 1234567890123456.8  (18 chữ số: mất chữ số cuối)

double giữ đúng được khoảng 15 tới 16 chữ số có nghĩa. Với dữ liệu tiền tệ cần chính xác tuyệt đối, hãy truyền dưới dạng chuỗi, hoặc truyền số nguyên theo đơn vị nhỏ nhất như mục 2.5.2.

Bài 2 — Chuẩn hoá lỗi về ProblemDetails ​

Một API cũ có ba endpoint trả lỗi theo ba định dạng khác nhau (xem lời giải). Viết lại cả ba theo ProblemDetails và chỉ ra client bớt được bao nhiêu nhánh xử lý. Endpoint GET /api/customers/search của CrmLab, trả 200 kèm success: false, là một ví dụ thật của kiểu B.

Tiêu chí hoàn thành: bạn đếm được số nhánh if mà client bớt được, và nêu được lợi ích ngoài việc gọn code.

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

Gợi ý. Viết ra đoạn code client phải có để xử lý cả ba định dạng, rồi viết lại đoạn đó khi cả ba dùng chung một định dạng. Chênh lệch chính là câu trả lời.

Lời giải — trước, ba định dạng khác nhau:

json
// Endpoint A
{ "error": "Không tìm thấy khách hàng" }

// Endpoint B
{ "success": false, "message": "Email đã tồn tại", "code": 1042 }

// Endpoint C
{ "errors": { "email": ["Email không hợp lệ"], "phone": ["Bắt buộc"] } }

Client phải viết:

ts
function getErrorMessage(res: any): string {
  if (res.error) return res.error;                        // nhánh 1
  if (res.success === false) return res.message;          // nhánh 2
  if (res.errors) return Object.values(res.errors).flat().join(", ");  // nhánh 3
  return "Đã có lỗi xảy ra";                              // nhánh 4
}

Sau, cùng một định dạng:

json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Không tìm thấy khách hàng",
  "status": 404,
  "detail": "Khách hàng với mã 42 không tồn tại.",
  "instance": "/api/v1/customers/42",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Client còn:

ts
const getErrorMessage = (p: ProblemDetails) => p.detail ?? p.title;

Bốn nhánh xuống một dòng. Nhưng lợi ích lớn hơn nằm ở chỗ khác:

  1. Một định nghĩa kiểu cho toàn bộ API. Client sinh được kiểu từ đặc tả OpenAPI và trình biên dịch TypeScript kiểm tra hộ, thay vì mỗi endpoint một hình dạng phải tự đoán.
  2. Lỗi kiểm tra dữ liệu vẫn diễn đạt được. ValidationProblemDetails bổ sung trường errors theo từng trường dữ liệu, nên trường hợp C không bị mất thông tin.
  3. Mã tương quan có chỗ đứng cố định. traceId luôn ở cùng một nơi, nên client hiển thị được cho người dùng và bộ phận hỗ trợ tra log ngay.
  4. Thêm endpoint mới không cần sửa client. Đây mới là lợi ích lớn nhất về lâu dài.

Bật trong ASP.NET Core:

csharp
builder.Services.AddProblemDetails(o =>
{
    o.CustomizeProblemDetails = ctx =>
        ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier;
});

AddProblemDetails() (có từ .NET 7) chỉ đăng ký dịch vụ tạo ProblemDetails. Phải thêm app.UseExceptionHandler() và app.UseStatusCodePages() thì các phản hồi lỗi chưa có thân (ví dụ 404 rỗng) mới được chuyển sang định dạng này. Bài 8.8 trình bày cách nối nó với xử lý ngoại lệ toàn cục.

Một điều cần cẩn thận. Trường detail đi tới người dùng cuối, nên không đặt thông tin nội bộ vào đó — tên bảng, câu truy vấn, dấu vết ngăn xếp. Những thứ đó ghi vào log phía server, và client chỉ nhận traceId để tra.

Bài 3 — Phân biệt vắng mặt và null ​

Viết một endpoint PATCH /api/customers/{id} xử lý đúng hai trường hợp: thân request có "phone": null nghĩa là xoá số điện thoại, và thân request không có trường phone nghĩa là giữ nguyên.

Tiêu chí hoàn thành: gửi {}, {"phone":"0912345678"} và {"phone":null} cho ba kết quả khác nhau, và bạn giải thích được vì sao DTO thông thường không phân biệt được.

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

Gợi ý. Với một DTO có thuộc tính string? Phone, cả hai trường hợp đều cho Phone == null sau khi giải mã. Vậy bạn cần một cách biểu diễn ba trạng thái thay vì hai: không gửi, gửi null, gửi giá trị.

Lời giải — vì sao DTO thường không đủ:

csharp
public class UpdateCustomerDto
{
    public string? Phone { get; set; }
}
Thân requestdto.PhoneÝ định thật
{}nullGiữ nguyên
{"phone": null}nullXoá

Hai ý định khác hẳn nhau nhưng cho cùng một giá trị. Thông tin đã mất ngay ở bước giải mã.

Cách 1 — kiểu bọc ba trạng thái:

csharp
public readonly struct Optional<T>
{
    public bool HasValue { get; }
    public T? Value { get; }
    public Optional(T? value) { HasValue = true; Value = value; }
}

public class UpdateCustomerDto
{
    public Optional<string?> Phone { get; set; }   // cần viết thêm JsonConverter riêng
}

Cách này cần tự viết một JsonConverter để System.Text.Json biết cách đọc Optional<T>, nên chỉ đáng làm khi API có nhiều endpoint PATCH.

Cách 2 — đọc thẳng JSON thô, đơn giản hơn nhiều. Một project dotnet new web hoàn chỉnh, dữ liệu để trong bộ nhớ cho gọn:

csharp
using System.Text.Json;

var phones = new Dictionary<int, string?> { [101] = "0901234567" };

var app = WebApplication.Create(args);

app.MapPatch("/api/customers/{id:int}", (int id, JsonElement body) =>
{
    if (!phones.ContainsKey(id)) return Results.NotFound();

    // TryGetProperty trả false khi trường VẮNG MẶT -> giữ nguyên
    if (body.TryGetProperty("phone", out var phone))
    {
        phones[id] = phone.ValueKind == JsonValueKind.Null
            ? null                  // có mặt, bằng null -> xoá
            : phone.GetString();    // có mặt, có giá trị -> cập nhật
    }

    return Results.Ok(new { id, phone = phones[id] });
});

app.Run();

Chạy bằng dotnet run --urls http://localhost:5050 rồi gửi lần lượt bốn thân request:

bash
for b in '{}' '{"phone":"0912345678"}' '{"name":"An"}' '{"phone":null}'; do
  echo "$b -> $(curl -s -X PATCH http://localhost:5050/api/customers/101 \
    -H 'Content-Type: application/json' -d "$b")"
done

Kết quả chạy thật:

text
{} -> {"id":101,"phone":"0901234567"}
{"phone":"0912345678"} -> {"id":101,"phone":"0912345678"}
{"name":"An"} -> {"id":101,"phone":"0912345678"}
{"phone":null} -> {"id":101,"phone":null}

TryGetProperty trả false khi trường vắng mặt, và ValueKind == Null khi trường có mặt nhưng bằng null. Đúng ba trạng thái cần thiết. Trong dự án thật, phần phones[id] = ... sẽ là cập nhật một entity rồi lưu qua EF Core (bài 8.10).

Cách 3 — dùng JSON Patch theo RFC 6902:

json
[
  { "op": "replace", "path": "/phone", "value": null },
  { "op": "remove",  "path": "/note" }
]

Định dạng này mô tả thao tác chứ không mô tả trạng thái mong muốn, nên không có chỗ nào mơ hồ. Đánh đổi là client phức tạp hơn và Swagger khó mô tả hơn.

Chọn cách nào. Với API nội bộ và số lượng trường ít, cách 2 gọn và đủ dùng. Với API công khai nhiều client, cách 1 cho DTO có kiểu rõ ràng và tài liệu tự sinh chính xác. Cách 3 hợp khi cần sửa cấu trúc lồng nhau hoặc sửa phần tử trong mảng.

Vì sao điều này quan trọng. JSON có null, nhưng một DTO C# thông thường làm mất khác biệt giữa "không gửi" và "gửi null". Không xử lý đúng thì người dùng không bao giờ xoá được số điện thoại đã nhập — một lỗi nhỏ nhưng gây khó chịu kéo dài, và rất hay bị bỏ qua khi thiết kế API.

Tự kiểm tra ​

Vì sao Id kiểu long phải trả về dưới dạng chuỗi trong JSON?

Vì JavaScript lưu mọi số bằng double, chỉ giữ chính xác số nguyên tới 2 mũ 53 trừ 1, tức khoảng 9 triệu tỉ. Số lớn hơn bị làm tròn mà không có lỗi nào được ném ra, nên client thao tác nhầm bản ghi. Trả dưới dạng chuỗi là cách duy nhất bảo đảm không mất chữ số nào.

JSON thiếu những kiểu dữ liệu nào?

Bốn thứ: số nguyên lớn vượt 2 mũ 53, ngày tháng, số thập phân chính xác kiểu decimal, và dữ liệu nhị phân. Ngày tháng phải quy ước theo ISO 8601 kèm múi giờ. Số tiền nên dùng chuỗi hoặc số nguyên theo đơn vị nhỏ nhất của loại tiền, kèm mã tiền tệ. Nhị phân phải mã hoá Base64 và chấp nhận phình thêm khoảng 33%.

Trường vắng mặt và trường null khác nhau thế nào?

Trong PATCH, trường có giá trị null nghĩa là hãy xoá giá trị đó, còn trường vắng mặt nghĩa là đừng đụng tới. Nếu model chỉ dùng string? thì cả hai đều thành null và bạn mất thông tin. Phải phân biệt bằng JsonElement hoặc kiểu bọc riêng — hoặc tránh hẳn bằng cách dùng PUT thay thế toàn bộ.

ProblemDetails giải quyết vấn đề gì?

Nó chuẩn hoá định dạng lỗi theo RFC 9457 (bản thay thế RFC 7807), nên client chỉ cần một cách xử lý lỗi cho mọi endpoint thay vì mỗi endpoint một kiểu. Công cụ bên ngoài cũng hiểu được mà không cần đọc tài liệu riêng. ASP.NET Core có sẵn Problem() và Results.Problem() sinh đúng định dạng này.

Vì sao không nên trả entity của EF Core trực tiếp?

Hai lý do. Entity có quan hệ hai chiều sẽ gây tham chiếu vòng khi serialize, trừ khi bật ReferenceHandler.IgnoreCycles. Và nguy hiểm hơn, nó vô tình phơi ra những cột nội bộ như PasswordHash hay cờ trạng thái mà client không nên thấy. Dùng DTO giải quyết cả hai.

Khi nào nên phân trang theo cursor thay vì theo số trang?

Khi bảng rất lớn. Phân trang theo số trang cần một câu COUNT để tính tổng số trang, và câu đó quét rất nhiều dòng trên bảng hàng triệu bản ghi. Phân trang theo cursor chỉ cần lấy n bản ghi kế tiếp sau một mốc, nên chi phí không tăng theo số trang.

Kết luận ​

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

  1. Số nguyên lớn trong JSON sai một cách im lặng. Không có ngoại lệ, không có cảnh báo — chỉ có chữ số cuối bị đổi.
  2. Đường dẫn là danh từ, method là động từ. Mọi quy ước REST khác đều bắt nguồn từ câu này.
  3. Lỗi có chuẩn rồi, đừng bịa thêm. ProblemDetails cho client một cách xử lý duy nhất.

Tham khảo ​

Điều hướng ​

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