🐾

OpenAPIでクライアントのプログラムを生成する

に公開

概要

OpenAPIを用いて、プロジェクト内のRest API実装から /v3/api-docs を取得し、それを基にTypeScript(Axios)クライアントを生成します。生成はGradleで行い、以下のようなフローで生成されます。

  1. Rest API起動
  2. APIドキュメントを /v3/api-docs から取得
  3. TypeScript(Axios)クライアント生成

環境

  • Java21
  • Gradle 8.5
  • Spring Boot 3.5.5

構成

openapi-generate-example
|-- generated                                           // OpenAPI Generatorの出力先
|-- src
|    |-- main
|         |-- java
|               |-- org
|                     |-- example
|                           |-- Application.java
|                           |-- ExampleController.java  // Rest API
|                           |-- ExampleRequest.java     // Rest API のリクエスト
|                           |-- ExampleResponse.java    // Rest API のレスポンス
|-- build.gradle.kts
|-- gradlew
|-- gradlew.bat
|-- settings.gradle.kts

Gradle

プラグイン

plugins {
    id("java")
    id("org.springdoc.openapi-gradle-plugin") version "1.9.0"
    id("org.openapi.generator") version "7.15.0"
}
プラグイン名 役割
java ビルド
org.springdoc.openapi-gradle-plugin /v3/api-docsを取得するタスクを提供
org.openapi.generator クライアントのソースコードを生成するタスクを提供

依存関係

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web:3.5.5")
  implementation("org.springframework.boot:spring-boot-starter-validation:3.5.5")
  implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
}

OpenAPI ドキュメント取得

openApi {
  apiDocsUrl.set("http://localhost:8080/v3/api-docs")
  outputDir.set(file("build/openapi"))
  outputFileName.set("openapi.json")
  waitTimeInSeconds.set(30)
}

http://localhost:8080/v3/api-docs から定義を取得し、build/openapi ディレクトリにOpenAPI定義を openapi.json というファイル名で作成します。

TypeScript Axiosクライアント生成

openApiGenerate {
  generatorName.set("typescript-axios")
  inputSpec.set(file("build/openapi/openapi.json").absolutePath)
  outputDir.set(file("generated").absolutePath)
  cleanupOutput.set(true)
  configOptions.set(
    mapOf(
      "withSeparateModelsAndApi" to "true",
      "useSingleRequestParameter" to "true",
      "apiPackage" to "api",
      "modelPackage" to "model"
    )
  )
}

tasks.named("openApiGenerate") {
  dependsOn("generateOpenApiDocs")
}

build/openapi/openapi.json を読み取って、generated ディレクトリにTypeScript Axiosクライアントを生成します。configOptions に生成時のオプションを設定します。

オプション名 説明
withSeparateModelsAndApi APIとモデルを別ファイルで生成する
useSingleRequestParameter API呼び出し時の引数をまとめてひとつのオブジェクトとして扱う
apiPackage 生成されるAPIの配置ディレクトリ
modelPackage 生成されるモデルの配置ディレクトリ

Java

Application.java

package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

ExampleController.java

package org.example;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
public class ExampleController {
    @PostMapping
    public ExampleResponse post(@RequestBody ExampleRequest request) {
        return new ExampleResponse("Example", 1L);
    }
}

ExampleRequest.java

package org.example;

import jakarta.validation.constraints.NotNull;

public record ExampleRequest(@NotNull String value1, Long value2) {
}

ExampleResponse.java

package org.example;

public record ExampleResponse(String value1, Long value2) {
}

実行方法

./gradlew openApiGenerate

生成結果

generated
|-- api
|     |-- example-controller-api.ts
|-- model
      |-- example-request.ts
      |-- example-response.ts

上記は生成結果からAPIとモデルを抜粋したものです。example-request.ts を確認すると以下のようになっており、ExampleRequest.java と対応していることが分かります。@NotNull を付けているため value1 は必須になっています。

export interface ExampleRequest {
    'value1': string;
    'value2'?: number;
}

クライアントでは以下のように使用します。

import axios, { AxiosError } from "axios";

import { ExampleControllerApiFactory } from '@/generated';
import type { ExampleRequest } from '@/generated';
import type { ExampleResponse } from '@/generated';

const client = axios.create({
  baseURL: 'http://localhost:8080',
  timeout: 30000,
  withCredentials: false,
});

const request: ExampleRequest = {
  value1: 'aaaaa',
  value2: 99999
}

const api = ExampleControllerApiFactory(undefined, undefined, client);
const response = await api.post({ exampleRequest: request });

参考

Discussion