ほとんどのWebサービスの開発では、Webサービスのフロントエンドを担当するフロントエンド・エンジニアとバックエンドを担当するバックエンド・エンジニアに分かれて開発します。フロントエンドは、バックエンド側が提供するHTTPベースのAPIを利用して構築されます。このため、フロントエンドの開発にはバックエンドのAPI仕様書が必須になります。
API仕様書は最近ではOpenAPI仕様のフォーマットに従って(ほとんどはyamlファイルで)書かれる事が多くなりました。ただし、バックエンドのプログラマとしてはAPIの仕様書を書くよりも、バックエンドのプログラムそのものを書きたいのが本音だと思います。そうは言っても、フロントエンド・エンジニアからはAPIの仕様を明文化してもらわないと開発しにくいとか、プロジェクトマネージャーから、インターフェースの部分の仕様だけでも先に決めてから実装すべきとかの圧力がかかる事が多いのも事実です。
そうやって仕方なく仕様書を書いていると、なぜ、Javaでプログラミングする時はJavadocで、Pythonでプログラミングする時はpydocでドキュメントを生成するのに、Web APIはエディタでyamlファイルを書いているのだろうと疑問に思えてきます。
実はSpring Bootでバックエンドの開発している場合は自動生成できるのです。本記事ではspringdoc-openapiを使って、OpenAPI仕様に従ったAPI定義書を自動生成する方法を、タスク管理管理サービスを例として解説します。
プロジェクトの作成
本記事では、Spring Bootのプロジェクトの作成から解説します。まず、Spring Initializrでプロジェクトを作成します。
下記の画面のようにプロジェクトの設定をします。 ADD DEPENDENCES...
ボタンをクリックして、以下をDependenciesに追加しています。
- Spring Web
GENERATE
ボタンをクリックしてプロジェクトをダウンロードし、適切なディレクトリにzipファイルを展開します。
springdoc-openapiの定義の追加
pom.xmlの
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.0.4</version> </dependency>
REST APIを仮実装
タスク管理のAPIを仮実装してみましょう。
まず、フロントエンドとのデータ転送オブジェクト(Data Transfer Object: DTO)のクラスを作成します。
package com.example.demo.presentation.dto; public record TaskResponseDto(String id, String title, String description) { }
package com.example.demo.presentation.dto; public record TaskRequestDto(String title, String description) { }
package com.example.demo.presentation.dto; import java.util.List; public record ListTaskResponseDto(List<TaskResponseDto> results) { }
コントローラクラスを以下のように作成します。
package com.example.demo.presentation; import com.example.demo.presentation.dto.ListTaskResponse; import com.example.demo.presentation.dto.TaskRequestDto; import com.example.demo.presentation.dto.TaskResponseDto; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.Arrays; @RestController public class TaskController { @PostMapping("/tasks") @ResponseStatus(value = HttpStatus.CREATED) public TaskResponseDto createTask(@RequestBody TaskRequestDto taskCreateRequestDto) { return new TaskResponseDto("004", taskCreateRequestDto.title(), taskCreateRequestDto.description()); } @GetMapping("/tasks/{id}") public TaskResponseDto getTask(@PathVariable String id) { return new TaskResponseDto(id, "%sのチケット".formatted(id), "%sのチケットを返す予定です".formatted(id)); } @GetMapping("/tasks") public ListTaskResponseDto listTask() { return new ListTaskResponseDto(Arrays.asList( new TaskResponseDto("001", "1番目のタスク", "タスクの説明"), new TaskResponseDto("002", "2番目のタスク", "タスクの説明。その2"), new TaskResponseDto("003", "3番目のタスク", "タスクの説明。その3") )); } @PutMapping("/tasks/{id}") public TaskResponseDto replaceTask(@PathVariable String id, @RequestBody TaskRequestDto taskCreateRequestDto) { return new TaskResponseDto(id, taskCreateRequestDto.title(), taskCreateRequestDto.description()); } @PatchMapping("/tasks/{id}") public TaskResponseDto updateTask(@PathVariable String id, @RequestBody TaskRequestDto taskRequestDto) { final TaskResponseDto taskResponseDto = new TaskResponseDto(id, taskRequestDto.title() == null ? "古いタイトル" : taskRequestDto.title(), taskRequestDto.description() == null ? "古い説明" : taskRequestDto.description()); return taskResponseDto; } @DeleteMapping("tasks/{id}") void deleteTask(@PathVariable String id) { // TODO 実装が必要ですが、とりあえず成功のレスポンスにします。 } }
API定義のアノテーションを追加
まず、アプリケーションのクラスにAPI仕様のタイトル等のアノテーションを追記します。
@SpringBootApplication @OpenAPIDefinition( info = @Info(title = "タスクAPI仕様", description = "タスクデータのAPI仕様です", version = "0.1.0", license = @License(name = "Apache License 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0.txt")), servers = {@Server(description = "Production Server", url = "https://demo.example.com")}) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
コントローラクラスにAPIのパスとオペレーションのアノテーションを追記します。
@RestController public class TaskController { @PostMapping("/tasks") @Operation(summary = "タスクを作成をする", tags = {"tasks"}, description = "タスクを作成します。", responses = {@ApiResponse(responseCode = "201", description = "作成されました。", content = @Content(mediaType = "application/json", schema = @Schema(implementation = TaskResponseDto.class)))}) @ResponseStatus(value = HttpStatus.CREATED) public TaskResponseDto createTask(@RequestBody TaskRequestDto taskCreateRequestDto) { return new TaskResponseDto("004", taskCreateRequestDto.title(), taskCreateRequestDto.description()); } @GetMapping("/tasks/{id}") @Operation(summary = "タスクを取得する。", tags = {"tasks"}, description = "IDを指定してタスクを取得します。", responses = {@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = TaskResponseDto.class)))}) public TaskResponseDto getTask(@Parameter(description = "タスクのID", required = true) @PathVariable String id) { return new TaskResponseDto(id, "%sのチケット".formatted(id), "%sのチケットを返す予定です".formatted(id)); } @GetMapping("/tasks") @Operation(summary = "タスクリストを取得する", tags = {"tasks"}, description = "タスクのリストを取得します。", responses = {@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ListTaskResponseDto.class)))}) public ListTaskResponseDto listTask() { return new ListTaskResponseDto(Arrays.asList( new TaskResponseDto("001", "1番目のタスク", "タスクの説明"), new TaskResponseDto("002", "2番目のタスク", "タスクの説明。その2"), new TaskResponseDto("003", "3番目のタスク", "タスクの説明。その3") )); } @PutMapping("/tasks/{id}") @Operation(summary = "タスクを更新する", tags = {"tasks"}, description = "タスクのリクエストされたプロパティを更新します。", responses = {@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = TaskResponseDto.class)))}) public TaskResponseDto replaceTask(@Parameter(description = "タスクのID", required = true) @PathVariable String id, @RequestBody TaskRequestDto taskCreateRequestDto) { return new TaskResponseDto(id, taskCreateRequestDto.title(), taskCreateRequestDto.description()); } @PatchMapping("/tasks/{id}") @Operation(summary = "タスクを置換する", tags = {"tasks"}, description = "タスクのすべてをリクエストされた値に置換します", responses = {@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = TaskResponseDto.class)))}) public TaskResponseDto updateTask(@Parameter(description = "タスクのID", required = true) @PathVariable String id, @RequestBody TaskRequestDto taskRequestDto) { TaskResponseDto taskResponseDto = new TaskResponseDto(id, taskRequestDto.title() == null ? "古いタイトル" : taskRequestDto.title(), taskRequestDto.description() == null ? "古い説明" : taskRequestDto.description()); return taskResponseDto; } @DeleteMapping("tasks/{id}") @Operation(summary = "タスクを削除する。", tags = {"tasks"}, description = "指定されたIDのタスクを削除します。", responses = {@ApiResponse(responseCode = "200", description = "OK")}) void deleteTask(@Parameter(description = "タスクのID", required = true) @PathVariable String id) { // TODO 実装が必要ですが、とりあえず成功のレスポンスにします。 } }
@Schema(description = "タスクデータ") public record TaskResponseDto(@Schema(description = "ID") String id, @Schema(description = "タスクの件名です") String title, @Schema(description = "タスクの詳細な説明です。") String description) { }
@Schema(description = "タスクデータ") public record TaskRequestDto(@Schema(description = "タスクの件名です") String title, @Schema(description = "タスクの詳細な説明です。") String description) { }
@Schema(description = "タスクリストデータ") public record ListTaskResponseDto(List<TaskResponseDto> results) { }
その他のアノテーションの詳細については、swagger-coreプロジェクトのWikiを参照してください。また、JSR-303 bean validation annotationも使えます。
API定義を取得する
以下のコマンドをプロジェクトの直下のディレクトリで実行するか、IDEを使って、アプリケーション・サーバを実行します。
./mvnw spring-boot:run
アプリケーション・サーバが起動したら、以下のコマンドを実行してAPI定義のYAMLファイルを取得します。
$ curl http://localhost:8080/v3/api-docs.yaml openapi: 3.0.1 info: title: タスクAPI仕様 description: タスクデータのAPI仕様です license: name: Apache License 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.txt version: 0.1.0 servers: - url: https://demo.example.com description: Production Server paths: /tasks/{id}: get: tags: - tasks summary: タスクを取得する。 description: IDを指定してタスクを取得します。 operationId: getTask parameters: - name: id in: path description: タスクのID required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/TaskResponseDto' put: tags: - tasks summary: タスクを更新する description: タスクのリクエストされたプロパティを更新します。 operationId: replaceTask parameters: - name: id in: path description: タスクのID required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/TaskRequestDto' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/TaskResponseDto' delete: tags: - tasks summary: タスクを削除する。 description: 指定されたIDのタスクを削除します。 operationId: deleteTask parameters: - name: id in: path description: タスクのID required: true schema: type: string responses: "200": description: OK patch: tags: - tasks summary: タスクを置換する description: タスクのすべてをリクエストされた値に置換します operationId: updateTask parameters: - name: id in: path description: タスクのID required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/TaskRequestDto' required: true responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/TaskResponseDto' /tasks: get: tags: - tasks summary: タスクリストを取得する description: タスクのリストを取得します。 operationId: listTask responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/ListTaskResponseDto' post: tags: - tasks summary: タスクを作成をする description: タスクを作成します。 operationId: createTask requestBody: content: application/json: schema: $ref: '#/components/schemas/TaskRequestDto' required: true responses: "201": description: 作成されました。 content: application/json: schema: $ref: '#/components/schemas/TaskResponseDto' components: schemas: TaskResponseDto: required: - title type: object properties: id: type: string description: ID title: maxLength: 125 minLength: 1 type: string description: タスクの件名です description: maxLength: 4096 minLength: 0 type: string description: タスクの詳細な説明です。 description: タスクデータ TaskRequestDto: required: - title type: object properties: title: maxLength: 125 minLength: 1 type: string description: タスクの件名です description: maxLength: 4096 minLength: 0 type: string description: タスクの詳細な説明です。 description: タスクデータ ListTaskResponseDto: type: object properties: results: type: array items: $ref: '#/components/schemas/TaskResponseDto' description: タスクリストデータ
ブラウザで http://localhost:8080/swagger-ui.html
を開くと以下のようにAPI仕様書のHTMLファイルとして見る事ができます。
この時点で、スタブ・サーバが完成している事になります。以下のようにリクエストすればデータの取得もできます。(以下ではjqで取得したJSONデータを整形しています)
$ curl -s http://localhost:8080/tasks | jq . { "results": [ { "id": "001", "title": "1番目のタスク", "description": "タスクの説明" }, { "id": "002", "title": "2番目のタスク", "description": "タスクの説明。その2" }, { "id": "003", "title": "3番目のタスク", "description": "タスクの説明。その3" } ] }
以上のように、springdoc-openapiを使うと、コードの概略とアノテーションを書くだけで、フロントエンド開発者が利用できるAPI仕様書とスタブ・サーバを作成できるようになります。