0.5 - Web API đầu tiên
Tóm tắt: Backend là chương trình chạy liên tục, chờ request HTTP và trả về dữ liệu, thường ở dạng JSON. Bài này tạo một Web API bằng
dotnet new webapi, thay endpoint dự báo thời tiết mẫu bằng API quản lý khách hàng nhỏ cho hệ CRM, rồi gọi thử bằng file.httpvàcurl. Bạn chưa cần hiểu hết từng dòng: mục tiêu là thấy được toàn bộ vòng request → code → response chạy trên máy mình. Module 2 giải thích HTTP, Module 8 và 9 giải thích ASP.NET Core một cách đầy đủ.
Mục tiêu bài học
Sau bài này bạn có thể:
- Tạo và chạy một project Web API, biết API đang lắng nghe ở cổng nào.
- Đọc
Program.cscủa Minimal API ở mức tổng quát: đăng ký dịch vụ,Build, khai báo endpoint,Run. - Viết endpoint
GETvàPOSTđơn giản, trả về mã trạng thái 200, 201, 404. - Gọi API bằng file
.httptrong trình soạn code và bằngcurl. - Xem tài liệu OpenAPI mà ASP.NET Core tự sinh ra.
Cần biết trước: đã làm xong 0.4 — Solution, thư viện và unit test. Với VS Code, đã cài extension REST Client (bài 0.1).
Nội dung bài học
0.5.1 — Tạo và chạy project
dotnet new webapi -n CrmApi
cd CrmApi
dotnet runUsing launch settings from D:\code\CrmApi\Properties\launchSettings.json...
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5107
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: DevelopmentKhác với chương trình console, lệnh này không kết thúc. API đứng chờ request cho tới khi bạn bấm Ctrl+C.
Dòng quan trọng nhất là Now listening on: http://localhost:5107. Số cổng (5107) được chọn ngẫu nhiên lúc tạo project, nên máy bạn sẽ ra số khác. Nó được lưu trong Properties/launchSettings.json. Ở mọi lệnh bên dưới, thay 5107 bằng số cổng của bạn.
Mở trình duyệt, vào http://localhost:5107/weatherforecast: bạn thấy một mảng JSON dự báo thời tiết ngẫu nhiên. Đó là endpoint mẫu của template.
0.5.2 — Các file trong project
CrmApi/
├── CrmApi.csproj ← Sdk="Microsoft.NET.Sdk.Web", có gói Microsoft.AspNetCore.OpenApi
├── Program.cs ← toàn bộ code của API
├── CrmApi.http ← các request mẫu để gọi thử
├── appsettings.json ← cấu hình (Module 8)
├── appsettings.Development.json ← cấu hình riêng khi chạy trên máy dev
└── Properties/
└── launchSettings.json ← cổng, môi trường khi chạy bằng dotnet run0.5.3 — API khách hàng cho hệ CRM
Tạo file Customer.cs cạnh Program.cs, chứa hai kiểu dữ liệu mà API dùng:
// Customer.cs
record Customer(int Id, string Name, string Email);
record CreateCustomerRequest(string Name, string Email);Rồi thay toàn bộ Program.cs bằng:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
// Dữ liệu tạm trong bộ nhớ: tắt app là mất. Module 13 sẽ thay bằng database.
var customers = new List<Customer>
{
new(1, "Nguyễn Văn An", "[email protected]"),
new(2, "Trần Thị Bình", "[email protected]"),
};
app.MapGet("/customers", () => customers);
app.MapGet("/customers/{id:int}", (int id) =>
customers.FirstOrDefault(c => c.Id == id) is { } customer
? Results.Ok(customer)
: Results.NotFound());
app.MapPost("/customers", (CreateCustomerRequest request) =>
{
var customer = new Customer(customers.Max(c => c.Id) + 1, request.Name, request.Email);
customers.Add(customer);
return Results.Created($"/customers/{customer.Id}", customer);
});
app.Run();Đọc theo bốn khối, từ trên xuống:
CreateBuildervàbuilder.Services...: chuẩn bị ứng dụng và đăng ký các dịch vụ nó cần. Ở đây chỉ có dịch vụ sinh tài liệu OpenAPI. Module 7 (Dependency Injection) giải thích kỹ phần này.builder.Build(): tạo ứng dụng từ những gì đã đăng ký.app.MapGet,app.MapPost: khai báo endpoint. Mỗi endpoint gồm một phương thức HTTP, một đường dẫn và một hàm xử lý.{id:int}là tham số lấy từ URL và phải là số nguyên. Khi request tới, ASP.NET Core tìm endpoint khớp rồi gọi hàm tương ứng.app.Run(): bắt đầu lắng nghe request.
Một số chi tiết đáng chú ý:
- Hàm xử lý trả về một đối tượng C# (danh sách
customers), ASP.NET Core tự chuyển thành JSON. Tên thuộc tínhNamethànhnametheo quy ước của JSON. Results.Ok,Results.NotFound,Results.Createdtrả về mã trạng thái HTTP tương ứng: 200, 404, 201. Mã 201 kèm headerLocationchỉ tới tài nguyên vừa tạo.- Ở
MapPost, tham sốCreateCustomerRequest requestđược ASP.NET Core đọc từ body JSON của request. recordlà cách khai báo ngắn cho một kiểu chỉ chứa dữ liệu. Bài 4.7 dạy kỹ về record.is { } customernghĩa là "nếu kết quả khác null thì gán vào biếncustomer". Đây là pattern matching, bài 4.8 sẽ học.
Dừng API cũ (Ctrl+C) và chạy lại bằng dotnet run.
Trong lúc học, dùng dotnet watch thay cho dotnet run: mỗi lần lưu file, API tự build và chạy lại, không cần dừng tay.
0.5.4 — Gọi API bằng file .http
Mở CrmApi.http, thay nội dung bằng:
@CrmApi_HostAddress = http://localhost:5107
### Lấy tất cả khách hàng
GET {{CrmApi_HostAddress}}/customers
### Lấy một khách hàng
GET {{CrmApi_HostAddress}}/customers/1
### Khách hàng không tồn tại
GET {{CrmApi_HostAddress}}/customers/9
### Tạo khách hàng mới
POST {{CrmApi_HostAddress}}/customers
Content-Type: application/json
{
"name": "Lê Văn Cường",
"email": "[email protected]"
}Phía trên mỗi request xuất hiện dòng chữ nhỏ Send Request. Bấm vào đó, kết quả hiện ở khung bên phải:
HTTP/1.1 201 Created
Location: /customers/3
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Lê Văn Cường","email":"[email protected]"}Gọi lại GET /customers để thấy khách hàng thứ ba đã có trong danh sách. Tắt API rồi chạy lại thì khách hàng đó mất: dữ liệu chỉ nằm trong bộ nhớ.
Dòng ### vừa ngăn cách các request, vừa làm tiêu đề cho từng request. Visual Studio và Rider đọc được file .http mà không cần cài thêm gì.
0.5.5 — Gọi API bằng curl
curl có sẵn trên Windows 10 trở lên, macOS và Linux. Mở một terminal khác, vì terminal cũ đang chạy API:
curl http://localhost:5107/customers[{"id":1,"name":"Nguyễn Văn An","email":"[email protected]"},{"id":2,"name":"Trần Thị Bình","email":"[email protected]"}]Thêm -i để thấy cả mã trạng thái và header:
curl -i http://localhost:5107/customers/9HTTP/1.1 404 Not Found0.5.6 — Tài liệu OpenAPI
Vào http://localhost:5107/openapi/v1.json. Đây là bản mô tả mọi endpoint của API theo chuẩn OpenAPI 3.1, do AddOpenApi() và MapOpenApi() sinh ra tự động:
{
"openapi": "3.1.1",
"info": {
"title": "CrmApi | v1",
"version": "1.0.0"
},
"paths": {
"/customers": {
"get": {Từ .NET 9, template không còn kèm giao diện Swagger UI. Nếu bạn vào /swagger theo các hướng dẫn cũ trên mạng, bạn sẽ nhận 404. Bài 9.3 hướng dẫn gắn giao diện xem tài liệu (Scalar hoặc Swagger UI) vào file JSON này.
if (app.Environment.IsDevelopment()) nghĩa là tài liệu chỉ được mở khi chạy trên máy dev, không lộ ra trên production.
0.5.7 — Lỗi thường gặp
Failed to bind to address ... address already in use. Một bản API khác vẫn đang chạy, thường do bạn mở hai terminal cùng dotnet run. Dừng bản cũ bằng Ctrl+C.
Trình duyệt cảnh báo chứng chỉ khi vào https://localhost:.... Chứng chỉ HTTPS dành cho máy dev chưa được tin cậy. Chạy dotnet dev-certs https --trust một lần. Trong bài này cứ dùng địa chỉ http:// cho đơn giản.
curl trên Windows gửi tiếng Việt bị lỗi 400. Gõ JSON có dấu thẳng trong lệnh curl -d '...' ở một số terminal Windows làm hỏng mã hóa ký tự, và server từ chối body. Dùng file .http, hoặc ghi body ra file UTF-8 rồi gửi bằng curl --data-binary @body.json.
POST trả về 400 dù tiếng Việt không có vấn đề. Body thiếu trường name hoặc email, hoặc thiếu header Content-Type: application/json. Hai trường này khai báo kiểu string (không có ?), nên ASP.NET Core coi chúng là bắt buộc.
Sửa code nhưng API vẫn trả kết quả cũ. Bạn đang chạy bằng dotnet run mà chưa khởi động lại. Dừng rồi chạy lại, hoặc dùng dotnet watch.
Danh sách rà soát cuối Module 0
- Chạy được API và gọi được cả ba endpoint bằng file
.http. - Gọi được ít nhất một endpoint bằng
curl -ivà đọc được mã trạng thái. - Mở được
/openapi/v1.json. - Giải thích được vì sao khách hàng vừa tạo biến mất khi khởi động lại API.
Bài tập áp dụng
Bài 1 — Chặn tên rỗng
Hiện tại POST /customers với "name": " " vẫn tạo được khách hàng. Sửa để API trả về 400 Bad Request kèm thông báo tiếng Việt khi tên rỗng hoặc chỉ có dấu cách, và tự cắt khoảng trắng thừa ở hai đầu tên hợp lệ.
Tiêu chí hoàn thành: thêm hai request vào file .http, một với tên rỗng (nhận 400), một với tên " Lê Văn Cường " (nhận 201 với tên đã được cắt khoảng trắng).
Gợi ý và lời giải — Bài 1
Gợi ý. string.IsNullOrWhiteSpace kiểm tra cả null, chuỗi rỗng và chuỗi chỉ có khoảng trắng. Trim() cắt khoảng trắng hai đầu.
Lời giải (thay endpoint MapPost):
app.MapPost("/customers", (CreateCustomerRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest("Tên khách hàng không được để trống.");
}
var customer = new Customer(customers.Max(c => c.Id) + 1, request.Name.Trim(), request.Email);
customers.Add(customer);
return Results.Created($"/customers/{customer.Id}", customer);
});HTTP/1.1 400 Bad Request
"Tên khách hàng không được để trống."Vì sao kiểm tra ở server dù giao diện web có thể đã kiểm tra? Vì ai cũng gọi thẳng được API bằng curl như bạn vừa làm, bỏ qua mọi kiểm tra ở giao diện. Server là lớp bảo vệ cuối cùng. Bài 8.5 dạy cách kiểm tra dữ liệu bài bản hơn.
Bài 2 — Xóa khách hàng
Thêm endpoint DELETE /customers/{id}: trả về 204 No Content khi xóa được, 404 khi không có khách hàng đó. Sau đó thử: xóa hết khách hàng rồi tạo một khách hàng mới.
Tiêu chí hoàn thành: xóa hai lần cùng một id thì lần đầu nhận 204, lần hai nhận 404. Tạo khách hàng sau khi đã xóa hết vẫn thành công.
Gợi ý và lời giải — Bài 2
Gợi ý. List.RemoveAll trả về số phần tử đã xóa. Phần thử "xóa hết rồi tạo mới" sẽ làm API sập: đọc stack trace như bài 0.3 để tìm nguyên nhân.
Lời giải (thêm trước app.Run()):
app.MapDelete("/customers/{id:int}", (int id) =>
{
var removed = customers.RemoveAll(c => c.Id == id);
return removed > 0 ? Results.NoContent() : Results.NotFound();
});Xóa hết rồi POST thì nhận lỗi 500. Log của API cho thấy InvalidOperationException: Sequence contains no elements, ném ra từ customers.Max(...): không thể lấy giá trị lớn nhất của một danh sách rỗng. Sửa cách tính id mới trong MapPost:
var nextId = customers.Count == 0 ? 1 : customers.Max(c => c.Id) + 1;
var customer = new Customer(nextId, request.Name.Trim(), request.Email);Hệ quả thực tế: tự tính id bằng "lớn nhất cộng một" còn có vấn đề khác. Hai request tạo cùng lúc có thể nhận trùng id. Database giải quyết chuyện này bằng cột tự tăng, bạn sẽ dùng nó từ Module 12 và 13.
Tự kiểm tra
Vì sao lệnh dotnet run của Web API không tự kết thúc như chương trình console?
Vì Web API là chương trình chạy liên tục để chờ request. app.Run() giữ chương trình sống và lắng nghe trên một cổng cho tới khi bạn dừng bằng Ctrl+C.
Mã 200, 201, 404 nghĩa là gì?
200 OK: thành công và có dữ liệu trả về. 201 Created: đã tạo tài nguyên mới, header Location chỉ tới nó. 404 Not Found: không có tài nguyên ở đường dẫn đó.
Endpoint nhận dữ liệu từ đâu?
Từ URL (như {id:int} trong đường dẫn) hoặc từ body của request (như CreateCustomerRequest trong POST, được đọc từ JSON). ASP.NET Core tự gắn dữ liệu vào tham số của hàm xử lý.
Vì sao vào /swagger lại nhận 404?
Từ .NET 9, template webapi chỉ sinh tài liệu OpenAPI dạng JSON ở /openapi/v1.json, không kèm giao diện Swagger UI. Muốn có giao diện thì phải thêm gói riêng (bài 9.3).
Kết luận
- Một Web API là một chương trình chạy liên tục, nhận request, gọi hàm xử lý tương ứng, trả JSON.
- Endpoint = phương thức HTTP + đường dẫn + hàm xử lý.
- Gọi thử bằng file
.httpđể lưu lại request cùng code, vàcurl -iđể xem cả mã trạng thái.
Bạn đã có đủ công cụ cho cả khóa học: SDK, trình soạn code, debugger, test và một API chạy được. Module 1 bắt đầu học C# một cách bài bản.
Tham khảo
- Tutorial: Create a minimal API with ASP.NET Core
- Use .http files in Visual Studio
- Generate OpenAPI documents
- REST Client for VS Code
- dotnet watch
Điều hướng
- Bài trước: 0.4 — Solution, thư viện và unit test
- Bài tiếp theo: 1.1 — Biến và kiểu dữ liệu
- Về module: Trang mục lục