Skip to content

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 .http và 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.cs của Minimal API ở mức tổng quát: đăng ký dịch vụ, Build, khai báo endpoint, Run.
  • Viết endpoint GET và POST đơn giản, trả về mã trạng thái 200, 201, 404.
  • Gọi API bằng file .http trong trình soạn code và bằng curl.
  • 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 ​

bash
dotnet new webapi -n CrmApi
cd CrmApi
dotnet run
text
Using 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: Development

Khá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 ​

text
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 run

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

csharp
// 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:

csharp
// 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:

  1. CreateBuilder và 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.
  2. builder.Build(): tạo ứng dụng từ những gì đã đăng ký.
  3. 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.
  4. 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ính Name thành name theo quy ước của JSON.
  • Results.Ok, Results.NotFound, Results.Created trả về mã trạng thái HTTP tương ứng: 200, 404, 201. Mã 201 kèm header Location chỉ 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.
  • record là 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 { } customer nghĩa là "nếu kết quả khác null thì gán vào biến customer". Đâ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:

http
@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:

text
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:

bash
curl http://localhost:5107/customers
text
[{"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:

bash
curl -i http://localhost:5107/customers/9
text
HTTP/1.1 404 Not Found

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

json
{
  "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 -i và đọ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):

csharp
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);
});
text
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()):

csharp
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:

csharp
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 ​

  1. 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.
  2. Endpoint = phương thức HTTP + đường dẫn + hàm xử lý.
  3. 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 ​

Điều hướng ​

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