OpenAPIとは？基本的な構造、書き方、使用例について調べてみた。

# はじめに
お久しぶりです。前回から２週間空いてしまいました。今回はOpenAPIの記事になります。
プログラミング学習コミュニティ「Progaku」では現在チーム開発を行なっております。そのチーム開発のタスクの中でOpenAPIを書くこと、CommitteeRailsを使用してRspecのテストへの反映を行いました。
どちらも初めて使用したので、色々と調べる中で知ったことをまとめていきたいと思います。

## OpenAPIとは？

https://www.openapis.org/

公式の見出しを翻訳した結果がこちら(笑)

> OpenAPI仕様は、HTTP APIを記述するための正式な標準を提供します。
これにより、APIがどのように動作するかを理解したり、クライアントコードを生成したり、テストを作成したり、設計標準を適用したり、その他多くのことができるようになります。

簡単にいうとAPIの仕様書になります。
ただ仕様書という枠に収まらず、仕様書の内容からテストを作成したり、コードを自動生成したり、様々なことができます。

OpenAPIを使用するメリットは以下のようなものがあるようです。
- 書き方の仕様が決まっているのでフォーマットを統一できる
- OpenAPIGeneratorでクライアントとサーバーのコードを自動生成
- サーバーサイドのAPIのコードを自動生成できる
- サーバーサイドの実装が終わっていなくてもモックサーバーを作成して実行できる
- OpenAPIの仕様書をHTMLなど生成して公開できる

いまいちわからないよって方は是非こちらの動画を見ていただければと思います。

https://www.youtube.com/watch?v=wujdvLPpSDs

ハンズオン形式で噛み砕いて説明してくださっているのでとても分かりやすかったです。

OpenAPIをベースにしたスキーマ駆動開発の実例はこちらの記事がとても勉強になりました。

https://zenn.dev/overflow_offers/articles/20220411-open-api-schema?redirected=1

定義されたOpenAPIドキュメントを主に行うことで
* 無駄なコミュニケーション（小さな確認など）が減る
* 出戻りが少なくなる
* モックサーバーの立ち上げ(Prism)やフロントのコードの自動生成(OpenAPI generator)などを使用することでバックエンドとフロントエンドを分離して開発していたとしても通信部分含め一気に開発できる
といったメリットがあるようです。

## 実際に書いてみる
概要がわかったところで実際に書いてみます。
今回はレシピの取得、投稿、更新、削除の簡単なAPIを例に記述しました。
yml形式かjson形式で書けるようですが一般的にはymlで書かれることが多いようです。
私は使用しませんでしたがGUIで書けるツールもあるようです

https://stoplight.io/solutions

構造としては大きく分けて5つにあります
- openapi
verを指定
これにより各種ツールがそのverに合わせて処理を行ってくれる。

- info
APIの情報
タイトル、バージョン、作成者　など　を記述する
titileとversionが必須項目となる

- servers(Server Object)
APIのベースURLを記述する
URL内に変数を含めることができる
ドメインやポート、バージョンなどを表現可能
一つずつ見ていきます

- paths
APIのエンドポイントについての情報を記述する
メソッド（GETなのかPOSTなのかなど）
クエリパラメータ
レスポンス
など

- components(Components Object)
OpenAPIを書く際に、同じような記述をしてしまう場面がある。
そういった時に、スキーマとして定義することで、再利用可能にし使用できる

記述したOpenAPIの確認には

https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi

vscodeのこちらのプラグインを使用しました。
インストールして右上の虫眼鏡マークを押すだけで表示できて便利です。
![スクリーンショット 2024-03-17 22.02.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3301763/028e78c0-1d94-f408-4215-f331fae53fc5.png)

このように表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3301763/20cb9451-3bbc-8b7d-39d0-4871c6a0e516.png)


## openapi, info
１つずつ見ていきます
```yml
openapi: 3.0.0
info:
  title: レシピAPI
  version: 1.0.0
  description: レシピを管理するためのAPI
```
ここでは先ほども触れましたがopenapiのverや情報を書いていきます
titleやversionは必須になるのでご注意ください。

## servers(Server Object)

```yaml
servers:
  - url: https://{environment}.example.com/v1
    description: 本番環境
    variables:
      environment:
        default: api
        description: "APIがホストされる環境"
        enum:
          - api          # 本番環境
          - api-dev      # 開発環境
          - api-staging  # ステージング環境
  - url: https://api.example.com/v2
    description: 次期バージョンの本番環境
  - url: http://localhost:8080/v1
    description: ローカル開発環境
```
ここではベースとなるURLを記述します。
urlは複数定義することができバージョンごとに分けたり、変数を定義して環境ごとに変更することもできます。

変数はvariablesオブジェクトを使用して定義します。
オブジェクトの下は変数名で今回はenviromentとしています。
enumを設定することで他にどのような値があるのか視覚的にわかる上にそれ以外の値が許可されなくなるので保守性が高まります。defaultオブジェクトでenumの初期値を指定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3301763/f4707f6e-9908-243e-0249-98881125a237.png)

プレビューするとこのような形になります。
選択した値を元にURLが生成されているのが分かります。

## paths
こちらがメインとなるAPIのエンドポイントについての情報を記述になります。
```
paths:
  /recipes:
    get:
      summary: 全てのレシピをリスト表示
      responses:
        '200':
          description: レシピのリスト
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Recipe'
```
このように
pathsオブジェクトの中に
階層構造で定義していきます。
エンドポイント、HTTPメソッド、summary、responsesのステータスコード、データ形式、データ型等
```yml
$ref: '#/components/schemas/Recipe'
```
このrefで参照しているのが再利用可能にしたcomponentsになります

## components
レスポンスボディ、リクエストパラメータなど
重複して記述するようなものも多く出てきます。
その際に使用するのがcomponentsになります。
文字通りcomponentsオブジェクトの中に定義します。
今回は
Recipe(レスポンスの定義),RecipeInput（リクエストボディの定義）,Error(エラー時のレスポンスの定義)の３つを定義しています。
```yml
components:
  schemas:
    Recipe:
      type: object
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        content:
          type: string
    RecipeInput:
      type: object
      required:
        - title
        - content
      properties:
        title:
          type: string
        content:
          type: string
    Error:
      type: object
      properties:
        errors:
          type: array
          items:
            type: string

```
これらを呼び出す際は
refを使用します。
componentsの前に#を使用する必要があるのに注意してください。
#は同じドキュメント内の特定の部分を指し示すためのフラグメントになります。
```yml
$ref: '#/components/schemas/Recipe'
```


## コード全体
上記を組み合わせて書いた結果がこちらになります
```yml
openapi: 3.0.0
info:
  title: レシピAPI
  version: 1.0.0
  description: レシピを管理するためのAPI
servers:
  - url: https://{environment}.example.com/v1
    description: 本番環境
    variables:
      environment:
        default: api
        description: "APIがホストされる環境"
        enum:
          - api          # 本番環境
          - api-dev      # 開発環境
          - api-staging  # ステージング環境
  - url: https://api.example.com/v2
    description: 次期バージョンの本番環境
  - url: http://localhost:8080/v1
    description: ローカル開発環境
paths:
  /recipes:
    get:
      summary: 全てのレシピをリスト表示
      responses:
        '200':
          description: レシピのリスト
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Recipe'
    post:
      summary: 新しいレシピを作成
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecipeInput'
      responses:
        '204':
          description: レシピが正常に作成されました
        '422':
          description: バリデーションエラー
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /recipes/{id}:
    put:
      summary: レシピを更新
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecipeInput'
      responses:
        '204':
          description: レシピが正常に更新されました
        '422':
          description: バリデーションエラー
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: レシピを削除
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: レシピが正常に削除されました
components:
  schemas:
    Recipe:
      type: object
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        content:
          type: string
    RecipeInput:
      type: object
      required:
        - title
        - content
      properties:
        title:
          type: string
        content:
          type: string
    Error:
      type: object
      properties:
        errors:
          type: array
          items:
            type: string

```

## まとめ
実際にこれらの定義を元にコードを生成したりテストをする際は
必須項目には適切に「required」を定義したり「additionalProperties: false」を指定して意図しないパラメータを許可しないようにしたり、フロントの自動生成コードの関数名となる「operationId」など意識して記述する必要があります。
これらは次回CommitteeをRspecに適用する記事で書いていきたいと思います。