はじめに
WINTICKET バックエンドチームの織田(@0daryo)です。
社内で運用していたAPI仕様記述を、API BlueprintからOpenAPI 3へ移行しました。
あわせてOpenAPI定義をベースとしたコードの自動生成にも対応しています。
本記事では移行に至った背景と手法、移行後の活用事例について紹介します。
移行の背景
WINTICKETのサーバー APIはGo言語で実装しています。
WINTICKETリリース当初からAPIに追加や変更があった際は、Go言語の実装に加えてAPI Blueprint形式で仕様を手動記述していました。
## カップ [/v1/keirin/races]
### 一覧 [GET /v1/keirin/races]
レース一覧を取得します。
+ Parameters
+ date: `20180701` (number,optional)
+ Response 200 (application/json)
+ Attributes
+ races - レース一覧
(...)
## Race
+ name: `WINTICKET杯` (string) - レース名
+ date: `20181201` (string) - 開催初日
+ number: 1 (number) - レース番号
(...)
この運用では、以下のような課題がありました:
- 実装と仕様の乖離・漏れが発生しやすい。
- 更新が手動で記述量に伴う保守コストが高い。
- API Blueprint自体の開発が止まっており周辺エコシステムとの連携が期待できない。
このため、実装と仕様を同期させた状態で管理できるよう、OpenAPIへの移行を進めました。
OpenAPIはツールエコシステムが充実しており、コードの自動生成や各種連携も期待できます。
OpenAPIへの移行に伴い、後述する管理画面用 API クライアント自動生成や E2E テスト用 API クライアント自動生成による開発・運用コストを削減を行いました。
移行プロセス
単純に手作業で移行する場合、数ヶ月単位の時間がかかることや、人的なミスの懸念があります。
また移行期間が長ければ長いほど、2システムに定義をコミットする必要が生じるなど、運用面でも悪影響があります。
そこで、API Blueprintの構造を解析してswag記法へ変換する内製ツールを開発し、移行作業を自動化しました。
移行対象
WINTICKETのAPIは合計約1000エンドポイントが対象でした。
- ユーザー向けAPI:約400エンドポイント。
- 管理画面向けAPI:約600エンドポイント。
記述方式とツール選定
以下の要件を満たすツールを探しました。
- GoのコードからAPI定義を生成、もしくはその逆の生成が可能。
- OpenAPI 3.0または3.1の定義の生成が可能。
下記のツールが候補となり、最終的にswaggo/swag を採用しました。
既存のWinTicketサーバーのフロントエンドはREST API構成でありgRPC定義は不要であったこと、
定義の記載がシンプルであり移行コスト削減が見込めることが決め手となりました。
シンプルな記載が可能になることに加えて、静的解析により構造体(下記例中の ‘api.KeirinListRacesResponse’ )のフィールドの情報が収集されるため運用コストが大幅に削減されます。
下記のような定義をもとにOpenAPI定義が生成されます。
// @Summary レース一覧取得
// @Description レース一覧を取得します。
// @Tags Keirin
// @Router /keirin/races [get]
// @Produce json
// @Success 200 {object} api.KeirinListRacesResponse
また、swaggo/swag 利用に際してWINTICKETでの利用に必要な修正をコントリビュートしました:
API Blueprintからswagへの変換
swaggo/swag の記法自体は前述のとおりシンプルなため、API Blueprintをプログラム上で扱うことができれば出力の難易度は高くありません。
API Blueprintをプログラムで扱うために以下の手法の検討を行いました。
- Drafter (API Blueprintパーサー)
- 複数ファイル対応に制限あり。
- 出力される構造を扱うコストが高い。
- 内製実装
- Markdownベースであるため、実装コストが小さい。
- WINTICKETには部分的なAPI Blueprintのvalidator実装があるため既存資産の活用が可能。
サービス内でカスタマイズした利用が多いことや内製実装コストが低かったため変換処理を実装しました。
整合性の担保
swag記載自体の漏れを検知する取り組みも行なっています。
WINTICKETではGoのルーティングライブラリにgo-chi/chiを利用しており、ルーティングとswag記法は次のような実装となります。
router := func(r chi.Router) {
r.Get("keirin/races", w.keirinListRaces)
}
(...)
// @Router /keirin/races [get]
func (w *web) keirinListRaces(rw http.ResponseWriter, r *http.Request) {
ルーティング実装とswag記法が整合するかCI上で検証しています。
下記がルーティング実装とOpenAPI定義からHTTPメソッドとパスを取得する実装例となります。
import "github.com/go-openapi/loads"
(...)
func newEndpointFromRoutes(parent string, routes chi.Routes) Endpoints {
if strings.HasSuffix(parent, "/*") {
parent = parent[:len(parent)-2]
}
rs := routes.Routes()
endpoints := make(Endpoints, 0, len(rs))
for _, route := range rs {
for method := range route.Handlers {
if method == "*" || strings.HasSuffix(route.Pattern, "/*") {
continue
}
endpoint := &Endpoint{
Method: method,
Path: parent + route.Pattern,
}
endpoints = append(endpoints, endpoint)
}
if route.SubRoutes != nil {
subEndpoints := newEndpointFromRoutes(parent+route.Pattern, route.SubRoutes)
endpoints = append(endpoints, subEndpoints...)
}
}
return endpoints
}
(...)
func newEndpointsFromOpenAPI(filePath string) (Endpoints, error) {
resource, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("openapi: failed to read resource file: %w", err)
}
doc, err := loads.Analyzed(resource, "")
if err != nil {
return nil, fmt.Errorf("openapi: failed to parse resource file: %w", err)
}
spec := doc.Spec()
const base = "/v1"
endpoints := make(Endpoints, 0)
for path, item := range spec.Paths.Paths {
methods := openapi.MethodsFromItem(item)
path = filepath.Join(base, path)
for _, method := range methods {
endpoint := &Endpoint{
Method: method,
Path: path,
}
endpoints = append(endpoints, endpoint)
}
}
return endpoints, nil
}
レンダリング
redoclyというツールを利用してOpenAPI定義をHTMLとして出力しGitHub Pagesでホスティングしています。
検索等のカスタマイズが容易でプラグインの開発も可能であるため柔軟に利用できます。
最終的に下記のような定義書が出力されます。
OpenAPIの活用事例
E2Eテスト用クライアントの自動生成
WINTICKET サーバーチームでは @takaokanbe を中心にAPI E2Eテストの実行基盤を整備しています。
テストシナリオでは、エンドユーザー/管理者としての操作を再現するため、多数のエンドポイントを使用しており、メンテナンス性の高い生成コードが有効です。
Go言語で実装しているE2Eテスト内のクライアントコードはOpenAPIを元に生成しています。
下記のツールが候補となりましたが、最終的に実行環境やOpenAPIのバージョンを理由に oapi-codegen を利用しています:
- oapi-codegen:OpenAPI 3.1に対応(OpenAPI3.0を推奨)。
- OpenAPITools/openapi-generator:生成コードにgo.modが含まれるためマルチワークスペース等の対応が必要。実行にJVMが必要なためCIやローカル環境に整備コストあり。
- ogen-go/ogen::Authorizationヘッダの注入のためにOpenAPI定義の修正が必要。
- kiota: OpenAPI 3.1 未対応(2025/03時点)
以下に生成例を示します。
// GetKeirinRaceWithResponse request returning *GetKeirinRaceResponse
func (c *ClientWithResponses) GetKeirinRaceWithResponse(ctx context.Context, params *GetKeirinRaceParams, reqEditors ...RequestEditorFn) (*GetKeirinRaceResponse, error) {
rsp, err := c.GetKeirinRace(ctx, params, reqEditors...)
if err != nil {
return nil, err
}
return ParseGetKeirinRaceResponse(rsp)
}
type GetKeirinRaceResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *ApiKeirinGetKeirinRaceResponse
}
// Status returns HTTPResponse.Status
func (r GetKeirinRaceResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
管理画面用TypeScriptクライアントの自動生成
WINTICKET Webチームでは@Cut0を中心にTypeScriptのAPIクライアントの自動生成に取り組んでいます。
これにより管理画面側のAPI更新追従コストを削減し、型安全な開発が可能になっています。
TypeScriptクライアントの作成には OpenAPITools/openapi-generator を活用しています。
このようにaxiosインスタンスを注入可能なコードを生成しています。
import axios from 'axios';
import { KeirinApiFactory } from '../api-client';
const APIClient = axios.create({
baseURL: BASE_URL,
});
export const keirinClient = KeirinApiFactory(undefined, undefined, APIClient);
const race = keirinClient.keirinKeirinRaceGet('123');
今後の活用予定
エンドユーザー用クライアントの自動生成
エンドユーザー用のアプリケーションでもAPIクライアントの自動生成対応を進めています。
管理面で対応したTypescriptに加えて、WINTICKETアプリで利用しているFlutter(Dart)でも同様の対応が可能です。
定義変更時の反映を自動化し、エンジニア間の認識齟齬を減らしています。
ローコードツール連携
WINTICKETでは社内ツールを中心にローコードツールの導入も検討しています。
RetoolやTooljetといったローコードツールでは、OpenAPIをデータソースとして利用可能です。
この連携により、プロトタイピングや簡易ダッシュボード作成などのユースケースでも再利用性が高まっています。
おわりに
本記事では、以下の取り組みについて紹介しました:
- API仕様管理のAPI BlueprintからOpenAPI 3へ移行
- swag による定義の自動生成とCI連携の整備
- 定義の活用によるE2Eテスト・フロントエンドコードの自動生成
- 今後のFlutter対応やローコードツール連携への展望
こうした取り組みを通じて、開発効率の向上や運用負荷を軽減することが可能となりました。
得られた知見が、同様の課題を抱える開発現場における改善の一助となれば幸いです。