OpenAPIを使ったフロントエンド開発 第1回 OpenAPIの概要と基本的な記述方法

APIの仕様をJSONやYAMLで記述して定義できるフォーマット、OpenAPIを解説します。まずはその概要と、基本的な記述を見てみましょう。

発行

著者 德田 和規 テクニカルディレクター
OpenAPIを使ったフロントエンド開発 シリーズの記事一覧

フロントエンドにおけるAPIの役割

現在のフロントエンド開発にあたり、Web API(以下API)はとても重要な役割を持っています。たとえばデータの取得・送信、認証、バリデーションなど、さまざまな機能をAPIが担当しています。

フロントエンドとAPIの開発は別々のチームで行われることが多いため、APIの仕様書をどのように管理しているかは、フロントエンド開発において重要視されていないことも珍しくありません。仕様書が独自のエクセルで管理されていることもあります。エクセルで管理されたものが一概に良くないということはありませんが、以下のような問題があります。

  • リクエストボディやレスポンスボディのサンプルJSONが記述しづらい
  • バージョン管理は内部にあるが、差分や変更履歴がわかりづらい
  • できればどのプロジェクトでも共通のフォーマットで管理したいが、独自の書き方であることが多いため、それができない

このような問題を解決するために、OpenAPIというものがあり、APIの仕様書をJSONまたはYAMLで定義できます。OpenAPIはSwaggerから派生したもので、現在はOpenAPIがSwaggerの後継としてメンテナンスされています。

補足:Swagger

SwaggerはRESTful APIを定義するために作られたオープンソース仕様ですが、OpenAPI Initiativeへ寄贈され名称がOpenAPIへと変わりました。現在ではSwaggerはSwagger UI、Swagger Editor、Swagger CodegenなどRESTful API定義をサポートするためのツールセットとして提供されています。

本シリーズでは、筆者がプロジェクトで実際にOpenAPIを使って開発を進めた経験をもとに、OpenAPIを使ったフロントエンド開発のメリットやデメリット、実際にどのように開発を進めたのかを紹介します。

なお、APIにもRESTful API、gRPC、GraphQLなどさまざまな種類がありますが、この記事ではRESTful APIを前提としています。

OpenAPIのフォーマットを扱うために

前述していますが、OpenAPIは、APIの仕様を定義するフォーマットです。OpenAPIを使うと、APIの仕様書をJSONやYAMLで記述できます。

フォーマットを扱うにあたって重要なことは、使っているエディタがフォーマットに対応しているか、ということが挙げられます。エディタに、シンタックスハイライト、バリデーション、フォーマットの整形、サジェストなどの機能があると作業効率が上がるからです。

筆者はWebStormを利用していますが、プラグインをインストールすることでOpenAPIのフォーマットに対応しています。VS Code(Visual Studio Code)でも同様のプラグイン「Open API(Swagger)Editor」があるので、OpenAPIを始めるにあたっては、エディタのプラグインをインストールすることをおすすめします。

なお、前述のSwaggerは現在ではOpenAPIを扱えるツールの一つという立ち位置になっており、エディタやAPIモックサーバーなどの機能を提供しています。また、他にも「Spotlight」などの管理ツールがあります。

なぜOpenAPIなのか

なぜ、エクセルなどによる独自フォーマットではなく、OpenAPIのほうがよいのか、大きく分けて3つの理由があると筆者は考えています。

  • Gitで管理できる
  • 共通フォーマットである
  • モックサーバーをローカルに立てることもできる

Gitで管理できる

Gitで管理できるというのは、現在では欠かせない要素です。バージョン管理ができるという大前提のほかにも、

  • 最新版がわかりやすい
  • 差分がわかりやすい
  • バックエンドとフロントエンドでの共同作業が行いやすい

などの理由があげられます。

作業差分が一目でわかるというのは問題点を見つけるのにも役立ちます。また、内容がアップデートされた場合に、新しくファイルが作られることもありません。たとえばproject_apilist_0420.xlsxのように日付管理された名前のファイルが増えていく、ということもないのです。

共通フォーマットである

APIの仕様書を独自のフォーマットで管理すると、プロジェクトごとにフォーマットが異なり、それぞれのプロジェクトで学習が必要になります。共通フォーマットであるOpenAPIを使うことで、プロジェクトごとに学習コストをかける必要がなくなります。

また、独自フォーマットの場合、たとえばブラウザ上で表示したいなどのときに、表示するための実装を自分で行う必要がでてきますが、OpneAPIのフォーマットであれば、対応したツールが有志たちによって作られています。ブラウザ上で表示したいという例であれば、Swagger UIを利用することで、定義したOpenAPIをブラウザで表示でき、さらには試しにリクエスト送ってレスポンスを見るということまでできます。

モックサーバーをローカルに立てることもできる

モック開発を先立って行うことも少なくないと思いますが、OpenAPIを使うことで、API仕様書からAPIモックサーバーを立てることもできます。本シリーズでも紹介します。

OpenAPIを使ったフロントエンド開発の準備

前置きが長くなってしまいましたが、実際にどのように定義して開発を進めていくのかを順番に見ていきましょう。

なお、今回筆者はOpenAPIに関わる外部サービスは使わず、エディタで定義ファイル(openapi.json)だけを編集しています。

エディタの準備

WebStormの場合は、プラグインにSwagger UIが組み込まれているため、エディタ上でプレビューまたはショートカットアイコンからブラウザで開くこともできます。

VS Codeの場合は、Swagger Viewerなどで同等の機能が提供されています。

プレビューにはSwaggerの一部であるSwagger UIを利用していますが、前述のとおり基本的にはWebStormもVS Codeもエディタのプラグイン内に組み込まれているため、あらためて個別にSwagger UIのインストール手順を踏む必要はありません。Swagger UIはOpenAPIのプレビューとしてわかりやすいため、以降プレビューはSwagger UIを利用しています。

JSONかYAMLか

JSONまたはYAMLで記述できるので、記述しやすい形式を選ぶとよいでしょう。オンラインのSwagger Editorでは、JSONとYAMLの相互変換ができるため、どちらかに統一することもできます。

筆者は当初YAMLで記述していましたが、より厳密な記述を行うためにJSONで記述するように変更しました。

OpenAPIで定義すること

OpenAPIは、APIの仕様に関わる、次のような箇所を網羅して定義します。

  • リクエスト・レスポンスのスキーマ定義
  • パスパラメータ・クエリパラメータ・ヘッダーの定義
  • レスポンスのヘッダーの定義
  • APIの認証方法の定義

OpenAPIの定義

OpenAPIでは6つのフィールドに分けて記述します。

ざっくりと触りを説明しますが、すべては説明しきれないので、詳細はOpenAPI Specificationなどを参照してください。

  1. バージョンを記載するopenapiフィールド(必須)
  2. メタデータ等を記載するinfoフィールド(必須)
  3. サーバー情報を記載するserversフィールド
  4. 各エンドポイントやメソッドを記載するpathsフィールド
  5. リクエスト・レスポンスのスキーマを記載するcomponentsフィールド
  6. securitytagなどの定義を記載するその他のフィールド

今回のサンプルで取り扱って解説するのは、1、2、4、5のフィールドです。

必須項目の定義

まず、必須項目のopenapiフィールドとinfoフィールドを見ていきましょう。

最初に、利用するOpenAPIのバージョンを指定します。利用できるバージョンは2系と3系がありますが、特に理由がない限り3系の利用が一般的です。執筆現在(2023年8月)では最新バージョンは3.1なのですが、筆者が利用しているツールが3.0系にしか対応していないため、3.0.3を利用しています。

infoフィールドはタイトルやファイルのバージョン、メタデータを記述します。サンプルでは簡潔にタイトルとバージョンのみを記述しています。

{
  "openapi": "3.0.3",
  "info": {
    "title": "API",
    "version": "1.0.0"
  }
}

pathsフィールドの定義の全体像

pathsフィールドでは、APIのエンドポイントとメソッドを定義します。どこで何を返すのか、どのようなパラメータを受け取るのか、どのようなレスポンスを返すのかを定義します。

まずは、ここで全体像を見てみましょう。/member/membersという2つのエンドポイントを定義しています。それぞれのエンドポイントがどう振る舞うのか、この後、順番に解説しています。なお、ここでは必須フィールドは省いています。

{
  "paths": {
    "/members": {
      "get": {
        "operationId": "getMembers",
        "summary": "ユーザー一覧を取得する",
        "description": "ユーザー一覧を取得します。",
        "responses": {
          "200": {
            "description": "ユーザー一覧を取得できた場合のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "ユーザーID",
                        "example": "1"
                      },
                      "name": {
                        "type": "string",
                        "description": "ユーザー名",
                        "example": "ピクグリ太郎"
                      }
                    },
                    "required": [
                      "id",
                      "name"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "エラー時のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "エラーコード"
                    },
                    "message": {
                      "type": "string",
                      "description": "エラーメッセージ"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/members/{member_id}": {
      "get": {
        "operationId": "getMember",
        "summary": "ユーザーを取得する",
        "description": "ユーザーを取得します。",
        "parameters": [
          {
            "name": "member_id",
            "in": "path",
            "description": "ユーザーID",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ユーザーを取得できた場合のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "ユーザーID",
                      "example": "1"
                    },
                    "name": {
                      "type": "string",
                      "description": "ユーザー名",
                      "example": "ピクグリ太郎"
                    }
                  },
                  "required": [
                    "id",
                    "name"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "エラー時のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "エラーコード"
                    },
                    "message": {
                      "type": "string",
                      "description": "エラーメッセージ"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

JSONレスポンスのサンプル

先にpathsフィールドで定義したAPIでは、/members/members/{member_id}という2つのエンドポイントとその振る舞いを記述しています。内容についてはこの後詳しく記載しますが、それぞれGETメソッドで呼び出した際のレスポンスは以下のとおりです。

正しく処理された場合とエラーの場合のJSONレスポンスです。

getMembers

[
  {
    "id": "1",
    "name": "ピクグリ太郎"
  }
]

getMember

{
  "id": "1",
  "name": "ピクグリ太郎"
}

エラー

{
  "code": "string",
  "message": "string"
}

pathsフィールドの各記述

それでは、pashsフィールドの詳細を見ていきましょう。

エンドポイントの記述

pathsの直下にはエンドポイント、その下にメソッド、さらに下に概要やレスポンスなど詳細を記述します。

  • エンドポイント: APIのURL(パス)に当たるところ
  • メソッド: エンドポイントに対して行う処理(GET、POST、PUT、DELETEなど)

サンプルは2つのエンドポイントを定義しています。

/members はユーザー一覧を取得するエンドポイントで、/members/{member_id}はユーザーを取得するエンドポイントです。

{
  "paths": {
    "/members": {
      ...
    },
    "/members/{member_id}": {
      ...
    }
  }
}

メソッドの記述

GETメソッドを定義するときは、大きく2つparametersresponsesを記述します。summarydescriptionはほとんどの箇所で利用できるので、運用にあわせてルールを決め記述するとよいでしょう。

  • operationId: メソッドのID。メソッドを識別するために利用する
  • summary: 概要を記述する
  • description: 詳細を記述する
  • parameters: パスパラメータ・クエリパラメータ定義
  • responses: レスポンスの定義
{
  "paths": {
    "/members": {
      "get": {
        "summary": "ユーザー一覧を取得する",
        "description": "ユーザー一覧を取得します。",
        "responses": {
          ...
        }
      }
    },
    "/members/{member_id}": {
      "get": {
        "summary": "ユーザーを取得する",
        "description": "ユーザーを取得します。",
        "parameters": [
          ...
        ],
        "responses": {
          ...
        }
      }
    }
  }
}

リクエストパラメータの記述

パスパラメータやクエリパラメータなどのリクエストパラメータがある場合はparametersを定義します。

  • name: パラメータ名。パスに記載したものに合わせて記述する。この場合は {member_id} に合わせている
  • in: パラメータの種類。パスパラメータの場合はpath、クエリパラメータの場合はquery
  • description: パラメータの説明
  • required: パラメータが必須かどうか
  • schema: パラメータのスキーマ。スキーマでは基本的にData Typeを記述する。スキーマの定義については後述
{
  "paths": {
    ...
    "/members/{member_id}": {
      "get": {
        ...
        "parameters": [
          {
            "name": "member_id",
            "in": "path",
            "description": "ユーザーID",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1"
            }
          }
        ],
        ...
      }
    },
    ...
  }
}

レスポンスの記述

responses内に記述します。直下にはHTTPステータスコードをキーにして記述します。

  • 200: 成功時のステータスコードなので、200の下には成功時のレスポンスを記述する
  • 400: エラー時のステータスコードなので、400の下にはエラー時のレスポンスを記述する

エラーについてはステータスコードそれぞれに詳細を書く必要があれば記述し、なければdefaultをキーにしてまとめて記述できます。

  • description: レスポンスの説明
  • content: レスポンスのコンテンツタイプ。ここではJSONを想定している。ContentTypeをキーにして、スキーマを記述する
{
  "paths": {
    "/members": {
      "get": {
        "responses": {
          "200": {
            "description": "ユーザーを取得できた場合のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "ユーザーID",
                      "example": "1"
                    },
                    "name": {
                      "type": "string",
                      "description": "ユーザー名",
                      "example": "ピクグリ太郎"
                    }
                  },
                  "required": [
                    "id",
                    "name"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "エラー時のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "エラーコード"
                    },
                    "message": {
                      "type": "string",
                      "description": "エラーメッセージ"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

スキーマの記述

スキーマでは、パラメータやレスポンスの構造を記述します。

  • type: 型を指定する。stringnumberarrayobjectなどがある
  • properties: プロパティを定義する。ここではidnameを持っていることを定義している

どちらも文字列として定義しているので、typeでString型を指定します。exampleではサンプル値を指定しています。

  • required: 必須プロパティを定義する。ここではidnameが必須であることを定義している

たとえばレスポンスボディで返すユーザーの場合、以下のようになっていました。

{
  "schema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "ユーザーID",
        "example": "1"
      },
      "name": {
        "type": "string",
        "description": "ユーザー名",
        "example": "ピクグリ太郎"
      }
    },
    "required": [
      "id",
      "name"
    ]
  }
}

サンプルJSON

ここまでをまとめると、以下のようなJSONが完成します。

{
  "openapi": "3.0.3",
  "info": {
    "title": "API",
    "version": "1.0.0"
  },
  "paths": {
    "/members": {
      "get": {
        "operationId": "getMembers",
        "summary": "ユーザー一覧を取得する",
        "description": "ユーザー一覧を取得します。",
        "responses": {
          "200": {
            "description": "ユーザー一覧を取得できた場合のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "ユーザーID",
                        "example": "1"
                      },
                      "name": {
                        "type": "string",
                        "description": "ユーザー名",
                        "example": "ピクグリ太郎"
                      }
                    },
                    "required": [
                      "id",
                      "name"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "エラー時のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "エラーコード"
                    },
                    "message": {
                      "type": "string",
                      "description": "エラーメッセージ"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/members/{member_id}": {
      "get": {
        "operationId": "getMember",
        "summary": "ユーザーを取得する",
        "description": "ユーザーを取得します。",
        "parameters": [
          {
            "name": "member_id",
            "in": "path",
            "description": "ユーザーID",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ユーザーを取得できた場合のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "ユーザーID",
                      "example": "1"
                    },
                    "name": {
                      "type": "string",
                      "description": "ユーザー名",
                      "example": "ピクグリ太郎"
                    }
                  },
                  "required": [
                    "id",
                    "name"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "エラー時のレスポンス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "エラーコード"
                    },
                    "message": {
                      "type": "string",
                      "description": "エラーメッセージ"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

次回はcomponentsフィールドを解説して、OpenAPIをひととおり完成させます。また、定義したOpenAPIを利用して簡易モックサーバーを立ててみましょう。