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ộtIdkiểulonghay mộtSnowflake IDsẽ 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ẩnProblemDetailscho lỗi, bốn thứ JSON không có sẵn (số nguyên lớn, ngày tháng,decimalchính xác, dữ liệu nhị phân), và chuyện trường vắng mặt khác trường bằngnull.
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
Idkiểulongphải trả về dưới dạng chuỗi trong JSON. - Trả lỗi theo chuẩn
ProblemDetailsthay 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/42Nguyê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 CRUDDò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
{ "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 đó:
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:
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:
{"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ờ:
{ "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ệ:
{ "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
{ "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:
{
"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"
}// 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
// 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:
{
"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
builder.Services.ConfigureHttpJsonOptions(o =>
{
o.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
o.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});Ba điều nên biết:
System.Text.Jsonphân biệt hoa thường theo mặc định, khác vớiNewtonsoft.Json. ASP.NET Core tự dùng bộ tuỳ chọnJsonSerializerOptions.Web(không phân biệt hoa thường, tên trường kiểucamelCase), nhưng khi tự gọiJsonSerializertrong ứng dụng console thì không: JSON có"name"sẽ không khớp thuộc tínhName. TruyềnJsonSerializerOptions.Webvà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:
JSON.parse('{"id":9007199254740993}').id // 9007199254740992
JSON.parse('{"id":9223372036854775807}').id // 9223372036854776000
Number.MAX_SAFE_INTEGER // 9007199254740991Kết quả chạy thật (Node 22):
| Giá trị gửi đi | JavaScript nhận được | Sai lệch |
|---|---|---|
9007199254740993 | 9007199254740992 | 1 |
9223372036854775807 (long.MaxValue) | 9223372036854776000 | 193 |
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ô:
curl -s http://localhost:5000/api/customers -H "Authorization: Bearer $TOKEN"[{"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):
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)))'[ 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:
curl -s -o /dev/null -w "%{http_code}\n" \
http://localhost:5000/api/customers/9007199254740992 -H "Authorization: Bearer $TOKEN"404Vì 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
idkhác vớiidthật trong database. - Gọi lại bằng id đó trả về
404như 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ó
idnhỏ.
Ba cách sửa, theo thứ tự nên dùng:
Đá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ụ:
csharpbuilder.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ả
totalPageshay 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"}:textThe 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.
Dùng
Guidlà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.Giữ
iddướ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:
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:
// 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:
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:
{
"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:
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:
- 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.
- Lỗi kiểm tra dữ liệu vẫn diễn đạt được.
ValidationProblemDetailsbổ sung trườngerrorstheo từng trường dữ liệu, nên trường hợp C không bị mất thông tin. - Mã tương quan có chỗ đứng cố định.
traceIdluô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. - 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:
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 đủ:
public class UpdateCustomerDto
{
public string? Phone { get; set; }
}| Thân request | dto.Phone | Ý định thật |
|---|---|---|
{} | null | Giữ nguyên |
{"phone": null} | null | Xoá |
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:
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:
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:
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")"
doneKết quả chạy thật:
{} -> {"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:
[
{ "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:
- 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.
- Đườ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.
- Lỗi có chuẩn rồi, đừng bịa thêm.
ProblemDetailscho client một cách xử lý duy nhất.
Tham khảo
- ASP.NET Core Web API — controller, routing, ProblemDetails
- System.Text.Json overview
- HTTP response status codes — chọn mã cho đúng
- RFC 9110 — HTTP Semantics
Điều hướng
- Bài trước: 2.4 — HTTP/HTTPS
- Bài tiếp theo: 2.6 — Cơ sở dữ liệu là gì
- Về module: Trang mục lục