API仕様書はOpenAPIで書こう！

# はじめに

APIを設計・開発するとき、仕様書をどうやって作るか悩んだことはありませんか？
ExcelやWordで書くのも一つの方法ですが、変更が発生したときのメンテナンスが大変だったり、開発者間・チーム間での共有がうまくいかなかったりと、課題が多いです。

そこでおすすめしたいのが **OpenAPI** です。
OpenAPI を使うことで、効率的かつ統一感のあるAPI仕様書を作成できるだけでなく、その仕様書からコードやドキュメントを自動生成することも可能です。

本記事では OpenAPI についてと、開発での活用方法を紹介します。

# OpenAPI とは？

https://swagger.io/specification/

OpenAPI (OpenAPI Specification) は、APIの仕様を記述するためのフォーマットです。
YAMLやJSON形式で記述され、APIのエンドポイントやリクエスト/レスポンスの構造を詳細に定義できます。
以前は、Swagger Specification として知られていました。

特徴としては以下のようなものがあります。

- 言語に依存しない
    - 実装言語を問わず使える
- ツールとの連携が豊富
    - Swagger UI で簡単にモックサーバーが建てられる
    - Postman を使って簡単にAPIテストが実施できる
    - コードジェネレーターも豊富で様々な言語のコードを自動生成できる
- 変更管理が容易
    - テキスト形式なので、Gitでバージョン管理ができる

# サンプル

サンプルコードを見ていきたいと思います。
細かい部分の説明はここでは省きます。
「こんな感じで書くのかぁ」とイメージだけ伝われば幸いです。

https://github.com/tgrhg/openapi-gen-sample


## エンドポイントの定義
メインとなるファイルです。
ユーザー情報に対する CRUD + リスト取得 を定義しています。

```yaml
# ./openapi.yaml

openapi: 3.1.1
info:
  title: Sample API
  description: API定義サンプル
  version: 0.0.1

security: []

servers:
  - url: http://localhost:8080/api
    description: ローカル環境
paths:
  /users:
    get:
      tags:
        - ユーザー
      summary: ユーザー情報リスト取得
      description: ユーザー情報の一覧を取得する。
      operationId: listUser
      responses:
        "200":
          description: ユーザー情報リスト
          content:
            application/json:
              schema:
                properties:
                  users:
                    type: array
                    items:
                      $ref: "schemas/User.yaml"
                type: object
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    post:
      tags:
        - ユーザー
      summary: ユーザー情報登録
      description: ユーザー情報を登録する。
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "schemas/User.yaml"
                - required:
                    - email
      responses:
        "201":
          description: ユーザー情報
          content:
            application/json:
              schema:
                $ref: "schemas/User.yaml"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
  /users/{id}:
    get:
      tags:
        - ユーザー
      summary: ユーザー情報取得
      description: 指定されたユーザー情報を取得する。
      operationId: getUser
      parameters:
        - name: id
          in: path
          description: ユーザーID。自身の情報を取得したい場合は`self`を指定する。
          required: true
          schema:
            type: string
            format: uuid
            example: ec00689e-3998-4df0-ae3e-416b052fafbd
      responses:
        "200":
          description: ユーザー情報
          content:
            application/json:
              schema:
                $ref: "schemas/User.yaml"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    delete:
      tags:
        - ユーザー
      summary: ユーザー情報削除
      description: 指定されたユーザー情報を削除する。
      operationId: deleteUser
      parameters:
        - name: id
          in: path
          description: ユーザーID
          required: true
          schema:
            type: string
            format: uuid
          example: c80f097d-2033-f09f-46d3-54ce1a559f83
      responses:
        "200":
          $ref: "#/components/responses/OK"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - ユーザー
      summary: ユーザー情報更新
      description: |
        指定されたユーザー情報を更新する。

        ※リクエストボディで指定されていない属性の値は更新しない。
      operationId: updateUser
      parameters:
        - name: id
          in: path
          description: ユーザーID
          required: true
          schema:
            type: string
            format: uuid
          example: c80f097d-2033-f09f-46d3-54ce1a559f83
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "schemas/User.yaml"
      responses:
        "200":
          $ref: "#/components/responses/OK"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

components:
  responses:
    OK:
      description: 成功
    Unauthorized:
      description: 認証失敗
    NotFound:
      description: 見つからない
    UnprocessableEntity:
      description: バリデーションエラー
```

## モデル・enum定義

エンドポイントの定義から参照されるモデルやenumを定義しています。
これらのファイルは別ファイルに定義することが可能です。
(必要であれば、エンドポイントの定義自体も分割可能)

```yaml
# ./schemas/User.yaml
title: ユーザー情報
type: object
properties:
  id:
    description: ユーザーID
    type: string
    format: uuid
    readOnly: true
    example: ec00689e-3998-4df0-ae3e-416b052fafbd
  lastName:
    description: 氏
    type: string
    example: 山田
  firstName:
    description: 名
    type: string
    example: 太郎
  tel:
    description: 電話番号
    type: string
    example: 090-1234-5678
  email:
    description: メールアドレス
    type: string
    example: test@mail.address.com
  status:
    $ref: "./enums/UserStatus.yaml"
  password:
    description: パスワード
    type: string
    writeOnly: true
    example: password
  createdAt:
    description: 作成日時
    type: string
    format: date-time
    readOnly: true
    example: "2023-01-01T00:00:00+09:00"
  updatedAt:
    description: 更新日時
    type: string
    format: date-time
    readOnly: true
    example: "2023-01-01T00:00:00+09:00"
```

```yaml
title: ユーザーステータス
type: string
description: |
  ステータス。
  - `valid` - 有効
  - `invalid` - 無効
  - `deleted` - 退会済み
enum: ["valid", "invalid", "deleted"]
x-enum-varnames:
  [
    "VALID",
    "INVALID",
    "DELETED",
  ]
example: valid
```

## ドキュメント生成のCI

OpenAPI の仕様書を Redocly というツールを使ってドキュメント化し、GitHub Pages にアップロードする GitHub Actions を定義しています。

```yaml
# ./.github/workflows/redoc.yaml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      # see https://github.com/actions/setup-node
      - name: node setting
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

      - name: install redoc
        run: npm install -g @redocly/cli

      - name: build open api
        run: |
          redocly build-docs ./openapi.yaml
          mkdir ./public
          mv ./redoc-static.html ./public/index.html

      - name: Setup Pages
        uses: actions/configure-pages@v5

      - name: Upload Artifacts to GitHub Pages
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./public

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
```

:::note

mainへのPush/PR作成でPagesを上書きしています。
実際に運用する際は、PR時は「Artifactsにアップロードするだけにする」など工夫が必要です。

:::


## 生成されたドキュメント

実際にCIで生成したドキュメントが以下です。

https://tgrhg.github.io/openapi-gen-sample/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3294382/3c3936ca-fcd2-2c00-c5b5-bfab72c9fd56.png)

如何でしょうか？ とても見やすいドキュメントになっていますね。


# OpenAPI からのコード生成

OpenAPI で API仕様を作成するメリットの１つとしてコードの自動生成があります。
例えば、API IF仕様からバックエンド/フロントエンド双方のアプリを開発する場合、仕様書をExcel等で作っていると、それに合わせたモデル定義を作成するのは、大変かつ、単純作業なので苦痛を伴います(笑)
また、変更があった場合に漏れなくメンテナンスしていくのは、長期的に見るとかなりのコストが掛かります。

それを、OpenAPI からの自動生成を利用することで、以下のように OpenAPI の変更をトリガーとして、バックエンドやフロントエンドのモデルの定義を自動生成するCIを組むことができます。

例えば、以下のようなCIを組めば、バックエンドとフロントエンドアプリからはpublish されたライブラリを参照すれば、最新のモデル定義を利用することができます。

```plantuml
start
:OpenAPIの仕様書をPush;
:CIで自動バージョンアップ;

fork
    :GitHub Pagesを生成する;
    :仕様書として閲覧する;
fork again
    :TypeScriptのコードを生成する;
    :npm として publish;
    :フロントエンド側で利用する;
fork again
    :Javaのコードを生成する;
    :jar として publish;
    :バックエンド側で利用する;
end fork

end
```

:::note

サーバーやリクエスト送信のコードを自動生成することも可能ですが、生成されたコードをそのまま利用するのは難しいです。
自動生成はモデルやenum定義のみに留めておくのをオススメします。

:::

## 自動生成例

OpenAPI Generator というツールを使って、TypeScript のモデル定義を生成する例です。

```bash
# ツールのインストール
brew install openapi-generator

# コード生成
openapi-generator generate -i openapi.yaml -g typescript-axios -o dist --global-property models,supportingFiles
```

<details>
  <summary>生成されたファイル (`api.ts` のみ抜粋)</summary>

```ts
// ./dist/api.ts

/* tslint:disable */
/* eslint-disable */
/**
 * Sample API
 * API定義サンプル
 *
 * The version of the OpenAPI document: 0.0.1
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
 */


import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';

/**
 * 
 * @export
 * @interface CreateUserRequest
 */
export interface CreateUserRequest {
    /**
     * ユーザーID
     * @type {string}
     * @memberof CreateUserRequest
     */
    'id'?: string;
    /**
     * 氏
     * @type {string}
     * @memberof CreateUserRequest
     */
    'lastName'?: string;
    /**
     * 名
     * @type {string}
     * @memberof CreateUserRequest
     */
    'firstName'?: string;
    /**
     * 電話番号
     * @type {string}
     * @memberof CreateUserRequest
     */
    'tel'?: string;
    /**
     * メールアドレス
     * @type {string}
     * @memberof CreateUserRequest
     */
    'email': string;
    /**
     * ステータス。 - `valid` - 有効 - `invalid` - 無効 - `deleted` - 退会済み 
     * @type {string}
     * @memberof CreateUserRequest
     */
    'status'?: CreateUserRequestStatusEnum;
    /**
     * パスワード
     * @type {string}
     * @memberof CreateUserRequest
     */
    'password'?: string;
    /**
     * 作成日時
     * @type {string}
     * @memberof CreateUserRequest
     */
    'createdAt'?: string;
    /**
     * 更新日時
     * @type {string}
     * @memberof CreateUserRequest
     */
    'updatedAt'?: string;
}

export const CreateUserRequestStatusEnum = {
    VALID: 'valid',
    INVALID: 'invalid',
    DELETED: 'deleted'
} as const;

export type CreateUserRequestStatusEnum = typeof CreateUserRequestStatusEnum[keyof typeof CreateUserRequestStatusEnum];

/**
 * 
 * @export
 * @interface ListUser200Response
 */
export interface ListUser200Response {
    /**
     * 
     * @type {Array<ListUser200ResponseUsersInner>}
     * @memberof ListUser200Response
     */
    'users'?: Array<ListUser200ResponseUsersInner>;
}
/**
 * 
 * @export
 * @interface ListUser200ResponseUsersInner
 */
export interface ListUser200ResponseUsersInner {
    /**
     * ユーザーID
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'id'?: string;
    /**
     * 氏
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'lastName'?: string;
    /**
     * 名
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'firstName'?: string;
    /**
     * 電話番号
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'tel'?: string;
    /**
     * メールアドレス
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'email'?: string;
    /**
     * ステータス。 - `valid` - 有効 - `invalid` - 無効 - `deleted` - 退会済み 
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'status'?: ListUser200ResponseUsersInnerStatusEnum;
    /**
     * パスワード
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'password'?: string;
    /**
     * 作成日時
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'createdAt'?: string;
    /**
     * 更新日時
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'updatedAt'?: string;
}

export const ListUser200ResponseUsersInnerStatusEnum = {
    VALID: 'valid',
    INVALID: 'invalid',
    DELETED: 'deleted'
} as const;

export type ListUser200ResponseUsersInnerStatusEnum = typeof ListUser200ResponseUsersInnerStatusEnum[keyof typeof ListUser200ResponseUsersInnerStatusEnum];
```

</details>


あとは、生成された `CreateUserRequest` や `ListUser200Response` を実際のリクエスト送信やレスポンス受信時に利用するだけです。

# まとめ

OpenAPI から仕様書の生成、コード(モデル定義)の自動生成を紹介しました。
少しでも良さが伝わっていれば幸いです。

機会があれば、次回はコード自動生成の具体例をさらに深掘りします。