🎋

Open API はじめました

2022/10/20に公開
2

始めに

こんにちは。株式会社ペライチ のサーバーサイドエンジニアの永見です。

開発プロセスに Open API を導入しました。ここにその知見を書いておきます。

背景

ペライチではメインのサイト作成サービスの他に、予約や顧客管理などさまざまなサービスを扱っています。各サブサービスはマイクロサービス化しており(全部ではないけど)、Web API サーバとして稼働しています。

開発の課題として以下がありました。

  • 課題 1 : 実装とドキュメントが一致していない

    API の設計を Git 外のドキュメントに記載していた
    まったく記載がないよりはマシですが、どうしてもメンテナンスが後手になり、
    実装と乖離している状態が定常化していました。

  • 課題 2 : フロントエンドとバックエンドの連携

    開発メンバーが増え、メンバー一人でバックとフロントの両方を受けもつことが少なくなり、
    フロントエンドとバックエンドの分野に、それぞれ担当範囲が分かれてきました。
    その結果、バックエンド側の API 実装をフロントエンド側で待つ場面が多くなりました。
    また、細かい部分で認識齟齬が起きていたりしました。

  • 課題 3 : API の数が多くなった

    大規模なプロジェクトが立ち上がったこともあり、全体の API の把握が難しくなっていました。

これらを解決するため、 Open API の導入を進めることにしました。

Open API とは

一言でいうなら、Web API のインタフェースを定義したファイルまたはその記法でしょうか。

誤解を生むのはマズイので公式ページの紹介文を持ってきました。
https://swagger.io/specification/

The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.

An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.

OpenAPI 仕様 (OAS) は、言語に依存しない標準の RESTful API へのインターフェイスを定義します。これにより、人間とコンピューターの両方が、ソース コードやドキュメントにアクセスしたり、ネットワーク トラフィックの検査を行ったりすることなく、サービスの機能を発見して理解できるようになります。適切に定義されていれば、コンシューマは最小限の実装ロジックでリモートサービスを理解し、対話することができます。

その後、OpenAPI 定義は、API を表示するためのドキュメント生成ツール、さまざまなプログラミング言語でサーバーとクライアントを生成するためのコード生成ツール、テスト ツール、およびその他の多くのユースケースで使用できます。

  • 言語に依存しない
  • RESTful API インタフェースの定義
  • 人間とコンピュータの両方が読める
  • 定義を元にさまざまなツールで使える

あたりがポイントですね。

Swagger との違い

Open API を調べていくと Swagger を目にするはずです。

  1. 元は Swagger という名のオープンソースのプロジェクト。
  2. SmatrBear Software 社が買収する。
  3. RESTful API の記述標準化を目指す団体 Open API Initiative が設立される。
    Microsoft, Google, IBM などが発足。
  4. SmatrBear Software 社が Swagger 2.0 を団体に寄贈し、ベースの仕様になる。
  5. Swagger から Open API に名称が変更される。

ですので、最新の仕様は Open API になります。
Swagger は当初から作られたツール群に名前が残っています。

使い方

API 定義ファイルを用意する。
MUST なのはこれだけです。
形式は JSON を YAML の両方があります。

私は直接書いてしまってますが、ツールから新規作成するのもよいでしょう。
実際、何人かの開発メンバーはツールオンリーで進めています。

Open API 関連のツールは数多くありますが、
対応しているバージョン、形式で差異があるため、
使う環境やルールは、ツールによって決めることになります。

方針

  • バージョンは Open API 3.0.3 とする (最新は 3.1)

  • YAML 形式で書く

  • 定義ファイルは API サーバのリポジトリに置く

  • 定義ファイルはファイル分割をしない

  • 既存の API は REST ルールにのっとっていなくても現状の通りに書く
    新規作成する場合にはなるべく REST のルールにしたがって設計する。

ツール

  • Stoplight Studio (エディタ/プレビュー/API 実行/エラーチェク/モック)

    多機能な便利なツール。
    下記の URL からダウンロードします。
    ちなみにスポットライトに見えますが、ストップライトです。綴り間違いではありません。

    https://stoplight.io/studio

  • Swagger Editor (エディタ/プレビュー/API 実行/エラーチェク/クライアント作成)

    Docker で起動する方法

    docker pull swaggerapi/swagger-editor
    docker run -d -p 80:8080 swaggerapi/swagger-editor
    

    基本部分だけなら VS Code のプラグイン OpenAPI (Swagger) Editor がお手軽です。
    https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi

  • Committee (テスト)

    Rails で API 定義をもとに RSpec でリクエストパラメータ、レスポンスの検証を行える Gem です。

    Gemfile

      gem 'committee-rails'
    

書き方

公式の GitHub リポジトリにあるドキュメント ( 3.0.3.md )が読みやすく、よく参照していました。

例を一度提示します。

openapi: 3.0.3
info:
  title: Exsample API
  description: サンプル API
  version: 1.0.0
  contact:
    name: Exsample Developent
    email: dev@example.com
servers:
  - url: 'http://localhost:88'
    description: 開発環境
paths:
  '/api/v1/users/{user_id}':
    get:
      description: ユーザ詳細
      operationId: UserShow
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
          description: ユーザID
      responses:
        '200':
          description: 成功時
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                    format: int64
                  name:
                    type: string
                  age:
                    type: integer
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
    put:
      description: ユーザ更新
      operationId: UserUpdate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: ユーザID
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                age:
                  type: integer
      responses:
        '200':
          description: 成功時
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                    format: int64
                  name:
                    type: string
                  age:
                    type: integer
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time

基本フォーマット

最初の階層に大項目を並べます。

openapi: 3.0.3  # [必須] Open API バージョンを指定します
info:           # [必須] API 定義の基本情報を記載します
  ...
servers:        # API サーバの情報を記載します
  ...
paths:          # [必須] エンドポイントのリクエストやレスポンスを記載する。メインとなる部分
  ...
components:     # 共通部分をここにまとめておきます
  ...

paths

paths
  '/api/v1/users/{user_id}/messages': # [必須] エンドポイントのURL。動的な箇所は {} 囲みでパラメータ名を入れます
    post:                           # [必須] HTTP メソッド名をつけます。get, post, put, delete など
      description: メッセージ作成
      operationId: MessageCreate    # [推奨] ツールが判別に使ってます。必須としているツールが多いです
      parameters:                   # パスやクエリやヘッダは parameters の項で指定します
        - name: user_id             # [必須] パラメータ名
          in: path                  # [必須] パラメータの種類。path, query, header, cookie など
          required: true
          schema:
            type: integer
          description: ユーザID
      requestBody:                  # POST や PUT メソッドで入れるリクエストボディはこちら
        content:                    # [必須] requestBody の場合は必須
          application/json:         # [必須] Content-type を指定
            schema:                 # [必須] この下の階層にスキーマを定義します。schema の項を参照。
              type: object
              required:
                - message
              properties:
                message:
                  type: string
      responses:                    # [必須] レスポンスを定義
        '200':                      # [必須] HTTP ステータスコードを指定します。指定以外の場合は default を使います
          description: 成功時
          content:                  # [必須] responseの の場合は必須
            application/json:       # [必須] Content-type を指定
              schema:               # [必須] この下の階層にスキーマを定義します。schema の項を参照。
                type: object
                properties:
                  result:
                    type: string

schema

たいていの API の場合は、最初に Object 型か Array 型を指定することが多いでしょう。

schema:
  type: object
  required:           # 必須とするプロパティを配列で指定。この必須はキーの存在を見ます。値が空かどうかは見ません。
    - id
  properties:         # [必須] object 型の場合は必須
    id:               # プロパティ名
      type: integer   # データ型
schema:
  type: array
  items:             # [必須] array 型の場合は必須
    type: integer    # データ型 この場合は [1, 2, 3] のような配列。上記の object 型の指定もできます

データ型は integer/number/string/boolean + object/array のみ指定できます。
integer や string は format で書式を指定できます。

id:
  type: integer
uuid:
  type: integer
  format: uuid
type format 説明
integer INT型
integer int32 符号付き32ビット INT型
integer int64 符号付き32ビット LONG INT型
number float 浮動点少数 FLOAT型
number double 浮動点少数 DOUBLE型
string 文字列
string uuid UUID形式の文字列
string email メールアドレス書式の文字列
string byte base64エンコードした文字列
string binary バイナリ
boolean ブール型
string date 日付型
string date-time 日時型
string password パスワード

他のプロパティも用意されています。

prefecture:
  type: string
  example: '東京都'            # 例となる値を示す
phone_number:
  type: string
  pattern: '^[0+]\d{8,10}$'    # フォーマットを正規表現で指定する
  nullable: true               # NULLを許可する
birth_year:
  type: integer
  minimum: 1900                # 最小値を指定する
  maximum: 2100                # 最大値を指定する

components

components 項目に共通項をまとめ、$ref で参照することで記載を省略できます。

openapi: 3.0.3
info:
  title: Exsample API
  description: サンプル API
  version: 1.0.0
  contact:
    name: Exsample Developent
    email: dev@example.com
servers:
  - url: 'http://localhost:88'
    description: 開発環境
paths:
  '/api/v1/users/{user_id}':
    get:
      description: ユーザ詳細
      operationId: UserShow
      parameters:
        - $ref: '#/components/parameters/PathUserId'
      responses:
        '200':
          description: 成功時
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
    put:
      description: ユーザ更新
      operationId: UserUpdate
      parameters:
        - $ref: '#/components/parameters/PathUserId'
      requestBody:
        $ref: '#/components/requestBodies/User'
      responses:
        '200':
          description: 成功時
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
  '/api/v1/users':                               # POSTを追加でもシンプルに書けます
    post:
      description: ユーザ作成
      operationId: UserCreate
      requestBody:
        $ref: '#/components/requestBodies/User'
      responses:
        '200':
          description: 成功時
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  parameters:
    PathUserId:
      name: user_id
      in: path
      required: true
      schema:
        type: integer
      description: ユーザID
  requestBodies:
    User:
      content:
        application/json:
          schema:
            type: object
            required:
              - name
            properties:
              name:
                type: string
              age:
                type: integer
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        age:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

Good & More

Good

  • ドキュメントに書く手間がほぼなくなった
    ツールに強力なプレビュー機能がついているので、API ドキュメントとして扱えます。
    API 定義とテストを連動させることで、実装との乖離も少なくできました。

  • リクエストやレスポンスのテストがシンプル
    Committee を使うと Rails のテストで
    データ型の細かい検証は、下記の 2 行で済みます。

    context 'when success' do
      it 'return expected body schema' do
        assert_request_schema_confirm       # リクエストの検証
        assert_response_schema_confirm 200  # レスポンスの検証
      end
    end
    
  • 現状の Web API の問題点が見える
    Open API が RESTful ルールであることを想定しており、
    API 定義を書くことで、 API の課題が見えてきました。

    • リクエストとレスポンスで同じ項目名でも型が異なる
    • データタイプに依存してリクエストやレスポンスが変わる
    • 複数のデータ型があるプロパティ

    これらのケースは一応、書く方法はありますが、かなりやりずらさを感じました。
    フロント側でも TypeScript で型定義しにくい面もあります。設計を見直すべき箇所です。

More

  • スキーマ駆動開発を進めるには?
    API 定義を書いてから実装を進める開発手法ですが、

    • Open API に対するメンバーの慣れが必要。
    • モックサーバとして動かすには example を充実させた方がよい。
      モックツールはデータ型に応じて、0string を入れて返してくれますが,
      フロント側の開発に使うには、ちょっと扱いにくい...。
      必須でなくてもスキーマの定義の example を書いた方が良いですね。
  • ツールからの API 実行について。

    • サーバ側の CORS 設定が必要。
      API 実行でレスポンスが返ってこず、気付くまでに結構ハマりました...。

まとめ

当初は難しそうに見えましたが、Open API の導入自体は意外と簡単でした。書き方も慣れれば特に問題ありません。
その先の、どうアプリケーションに連動させるか、開発プロセスにどう組み込むかで悩む時間の方が多かったです。

API 定義からクライアント側ソースコード、 Wiki ドキュメントを自動作成する高度なツールもあります。
使いこなすほど効率化できるので、開発のなるべく早い段階で入れることをお勧めします!

採用情報

現在エンジニア募集しています!

▼ 採用ページ
https://recruit.peraichi.co.jp/

▼ 選考をご希望の方はこちら(募集職種一覧)
https://hrmos.co/pages/peraichi/jobs?category=1629135637016141824&utm_source=techblog&utm_medium=referral&utm_campaign=article-01ges56ak79g2rket9wm27w9pc

▼ まずはカジュアル面談をご希望の方はこちら
https://hrmos.co/pages/peraichi/jobs/0000029?utm_source=techblog&utm_medium=referral&utm_campaign=article-01ges56ak79g2rket9wm27w9pc

募集中の職種についてご興味がある方は、お気軽にお申し込みください(CTO がお会いします)

ペライチ

Discussion

いもけんぴいもけんぴ
paths:
  '/api/v1/users/{user_id}':
    get:
      description: ユーザ詳細
      operationId: UserShow
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: interger
          description: ユーザID

書き方の項目部分にtypoがあります。
interger --> integer

NAGAMI-PERAICHINAGAMI-PERAICHI

>いもけんぴさん

ご指摘ありがとうございます。
遅くなりましたが修正しました!