Note: see the link below for the English version of this article.

https://duongnt.com/openapi-generator

openapi-generator

Việc viết code để gọi API tiêu tốn tương đối nhiều thời gian. Chúng ta vừa phải chọn đúng lệnh GET/POST/PATCH… và gửi request tới đúng endpoint, vừa phải truyền đủ tham số với format phù hợp, lại còn phải đọc response trả về và trích xuất ra dữ liệu cần thiết. Ngoài ra ta cũng phải xử lý lỗi đường truyền và lỗi do server trả về. Với OpenAPI Generator, ta có thể tự động hóa toàn bộ các công việc trên.

Trong bài hôm nay, chúng ta sẽ dùng OpenAPI Generator để tự động sinh API client cho 2 API mà chỉ dùng tới file OpenAPI của chúng. Tất cả code trong bài sẽ sử dụng C# .NET Core.

Các bạn có thể tải toàn bộ code trong bài từ đường link bên dưới. Repository đó có chứa tất cả API, client được sinh tự động, và một chương trình demo.

https://github.com/duongntbk/OpenAPIGeneratorDemo

Cài đặt OpenAPI Generator

Việc cài đặt OpenAPI Generator là rất đơn giản. Tôi dùng npm để cài bản CLI trên môi trường Windows.

npm install @openapitools/openapi-generator-cli -g

Nhưng các bạn cũng có thể sử dụng Homebrew, Docker, Bash Script, hoặc cài trực tiếp từ Maven. Xin tham khảo đường link này để biết thêm chi tiết.

Ta còn cần cài cả bản Java Development Kit mới nhất, các bạn có thể tải bộ cài từ trang web của Oracle.

Sau đó, ta có thể xác nhận xem OpenAPI Generator đã được cài đúng cách hay chưa bằng lệnh sau.

openapi-generator-cli version

Đây là kết quả sau khi tôi cài phiên bản 5.3.0.

Did set selected version to 5.3.0
5.3.0

Tự động sinh client

OpenAPI Generator có thể tự động sinh API client, server, tài liệu hay file cấu hình của một API mà chỉ dựa trên file OpenAPI. Trong bài này, ta sẽ sinh client cho 2 API dưới đây.

  • Maritimum: API này trả về dữ liệu test về tàu biển và cảng. File OpenAPI của nó nằm tại đây.
  • Pecunia: API này trả về dữ liệu test về người và tài khoản. File OpenAPI của nó nằm tại đây.

Lệnh để sinh client cho Maritimum là như sau.

openapi-generator-cli generate -i Maritimum/doc/openapi.yaml -g csharp-netcore -o <thư mục đầu ra> --package-name MaritimumClient

Ý nghĩa của từng tham số.

  • i: Đường dẫn tới file openapi.yaml của Maritimum.
  • g: Ngôn ngữ của client. Ta truyền giá trị csharp-netcore để sinh client viết bằng C# .NET Core. OpenAPI Generator còn hỗ trợ nhiều ngôn ngữ khác. Các bạn có thể tham khảo danh sách đầy đủ tại đây.
  • package-name: namespace cho client của ta.

Danh sách các file được sinh tự động

Thư mục đầu ra có chứa tài liệu về client viết bằng Markdown, code cho unit test, và code của client. Ta sẽ tập trung vào thư mục chứa code của client.

  • Thư mục Api: đây là các lớp mà ta sẽ dùng để gọi API.
  • Thư mục Client: đây là các lớp hỗ trợ việc gửi request HTTP và xử lý response.
  • Thư mục Model: đây là các lớp để map với object trong request và response.

Có thể thấy là trong folder Api có 2 lớp. ShipApi được dùng để gọi các endpoint với tag là ship. Còn PortApi được dùng để gọi các endpoint với tag là port. Nếu các endpoint của ta không có giá trị tag thì tất cả chúng sẽ được gộp vào cùng 1 class với tên gọi là DefaultApi. Theo tôi thì ta nên chia các endpoint vào nhiều lớp riêng biệt dựa trên giá trị tag. Điều này giúp giảm kích cỡ của từng lớp và giúp code trở nên dễ đọc hơn.

Tương tự trên, ta dùng lệnh sau để sinh client cho Pecunia.

openapi-generator-cli generate -i Pecunia/doc/openapi.yaml -g csharp-netcore -o <thư mục đầu ra> --package-name PecuniaClient

Dùng client vừa sinh để gọi endpoint

Tạo object thuộc lớp XXXApi

Như đã nói ở trên, ta dùng lớp XXXApi để gọi các endpoint của API. Ví dụ: để lấy thông tin về tàu biển từ Maritimum, ta cần tạo object của lớp ShipApi. Cách nhanh nhất là dùng hàm khởi tạo này.

var shipApi = new ShipApi("https://localhost:5003"); // https://localhost:5003 là địa chỉ của Maritimum

Nếu muốn tùy biến ShipApi, ta có thể tạo một đối tượng thuộc lớp Configuration và truyền nó vào hàm khởi tạo này.

var configuration = new Configuration
{
    BasePath = "https://localhost:5003",
    AccessToken = "<access token>",
    UserAgent = "<custom user-agent",
    Timeout = 500_000, // đặt giới hạn timeout là 500.000 mili-giây
    // ...
};
var shipApi = new ShipApi(configuration);

Ta cũng có thể tùy biến cả quá trình gửi và nhận HTTP request bằng cách dùng hàm khởi tạo này. Lúc này, ta cần tự mình viết HttpClient.

Gọi endpoints

Đầu tiên, ta sẽ gọi endpoint ships.index. Endpoint này không cần tham số và nó trả về tất cả tàu biền mà API có. Hàm tương ứng trong lớp ShipApiShipsIndexAsync (cũng có cả hàm ShipsIndex, nhưng tạm thời ta chỉ quan tâm tới hàm không đồng bộ).

var allShips = await shipApi.ShipsIndexAsync(); // allShips có dạng List<Ship>

Có thể thấy cách gọi endpoint là rất đơn giản, nhưng ships.index là endpoint dễ gọi nhất. Tiếp theo, ta sẽ gọi ships.report, endpoint này phức tạp hơn. Ta cần truyền vào tham số home_port_uuid, nếu muốn ta có thể truyền vào cả giá trị tải trọng tối thiểu (min_tonnage) và tối đa (max_tonnage). Hơn nữa, ta phải dùng HTTP-Post thay vì HTTP-Get khi gọi endpoint này. Dưới đây là cách gọi ships.report bằng client.

var homePortUuid = new Guid("28153b3b-f3ac-45d1-820d-fec983a9aa56");
var maxTonnage = 35_000; // Chỉ trả về các tàu có tải trọng lớn hơn 35,000
var shipReportRequest = new ShipReportRequest(homePortUuid, maxTonnage);
var someShips = await shipApi.ShipsReportAsync(shipReportRequest); // someShips có dạng List<Ship>

Việc gọi ships.report cũng không khó hơn nhiều so với việc gọi ships.index. Hàm ShipsReportAsync sẽ tự tạo request dựa trên tham số ta truyền vào và dùng HTTP-Post để gửi request đó tới đúng endpoint. Sau đó nó sẽ tự chuyển response mà API trả về thành dạng List<Ship>.

Có lẽ các bạn cũng đã nhận ra là ta không truyền vào giá trị minTonnage. Đây là một tham số không bắt buộc của ships.report và property tương ứng là ShipReportRequest.MinTonnage có kiểu là Nullable<int>. Nói một cách tổng quát, nếu trong file OpenAPI một property được đánh dấu là nullable: true thì property tương ứng trong client sẽ là Nullable<T>.

Khái quát về các lớp model

Như ta thấy trong phần trước, các component trong file OpenAPI được map với lớp model tương ứng trong client. Ví dụ: component Ship được map với class Ship. Nhưng các lớp model này không chỉ chứa danh sách các property. Dưới đây là code để in ra danh sách các tàu biển.

foreach (var ship in someShips)
{
    Console.WriteLine(ship.ToString());
}

Lớp Ship đã định nghĩa lại hàm ToString để in ra toàn bộ thông tin về từng tàu biển. Đoạn code trên sẽ in ra thông tin dưới đây.

class Ship {
  Uuid: e85382d2-552c-4745-b7fe-114c2d87e4c3
  Name: HQ-04
  HomePortUuid: 28153b3b-f3ac-45d1-820d-fec983a9aa56
  Tonnage: 40000
}

class Ship {
  Uuid: f336a003-fb63-48b3-8180-80c9f5541fc0
  Name: Seiraimaru
  HomePortUuid: f0d4d067-c806-4b1d-a8e6-2080504be61b
  Tonnage: 100000
}

Hơn nữa, lớp này còn định nghĩa lại cả hàm Equals để so sánh 2 tàu dựa trên các property thay vì so sánh reference.

var uuid = Guid.NewGuid();
var homePortUuid = Guid.NewGuid();
var name = "dummyName";
var ship1 = new Ship(uuid, name, homePortUuid);
var ship2 = new Ship(uuid, name, homePortUuid);

Console.WriteLine(ship1.Equals(ship2)); // Sẽ in ra "true"

Tất nhiên là ta vẫn có thể so sánh reference nếu muốn.

Console.WriteLine(object.ReferenceEquals(ship1, ship2)); // Sẽ in ra "false"

Xử lý exception

Tất cả exception xảy ra khi gọi API đều được bọc trong kiểu ApiException. Lớp này chứa mã của lỗi HTTP, nội dung của request dưới dạng json, thông báo lỗi và HTTP header. Ta sẽ thử gọi Maritimum với một UUID không tồn tại.

try
{
    await ShipApi.ShipsShowAsync(Guid.NewGuid());
}
catch (ApiException ex)
{
    Console.WriteLine($"Error code: {exception.ErrorCode}");
    Console.WriteLine($"Error content: {exception.ErrorContent}");
    Console.WriteLine($"Error message: {exception.Message}");
}

Đoạn code trên sẽ in ra thông tin dưới đây.

Error code: 404
Error content: "Cannot find Ship with Uuid: <giá trị UUID>"
Error message: Error calling ShipsShow: "Cannot find Ship with Uuid: <giá trị UUID>"

Kết thúc

Đôi khi lười một chút lại tốt, và OpenAPI Generator cho phép ta "lười" mà vẫn được việc. Ta có thể thiết lập workflow để quét định kỳ các repository chứa API. Mỗi khi một file OpenAPI thay đổi, ta kích hoạt process ở background để sinh lại API client tương ứng. Sau đó client sẽ được tự động cập nhật lên repository của riêng nó. Cách này cho phép ta đảm bảo API client luôn luôn là bản mới nhất.

A software developer from Vietnam and is currently living in Japan.

3 Thoughts on “Tự động sinh API client bằng OpenAPI Generator”

    • Chào Tùng,
      Các tool trong link bạn gửi là để scan code và tự động tạo file specification, tức là file openapi.yaml như trong demo của mình.
      Còn tool OpenAPI Generator là để đọc file openapi.yaml đó và tự động sinh ra code client để dễ sử dụng API hơn. Người dùng thay vì phải tự tạo request HTTP, gửi request, và đọc response… thì bây giờ họ chỉ cần gọi 1 hàm trong client do OpenAPI Generator sinh tự động.
      Với cả tool này không phải do mình tự làm đâu. Đây là một project open source tương đối nổi tiếng. Link.

Leave a Reply