見出し画像

OpenAPIでREST APIを設計してみた

はじめに

こんにちは、SHIFT の開発部門に所属しているKatayamaです。今期から転属になり、開発を担当していくことになりました。

現在、基本的な事から学ぶ研修中です。開発部門では新しく学ぶことがたくさんあり、それらを自身の振り返りアウトプットとして発信していけたらと思います。記事が溜まったら、noteのマガジンにもまとめる予定です。

今回はOpenAPIでREST APIを1から設計してみて、REST APIの設計に関して基本的な事の理解を深めました。

言葉の意味について理解を深める

REST APIについてその意味を理解する前に、それを理解するための予備知識があるようなのでそれ(OpenAPI, REST)について理解をしてから、REST APIとは何なのか?を理解していきたい。

OpenAPIとは

OpenAPI Specificationの事で、API(REST API)を定義するための標準仕様。
つまり、APIをどのような文書形式で書くのか?どんな項目があるのか?というフォーマットのルールそのものの事で、実態として何かOpenAPIというものがあるわけではない。

REST とは

Representational State Transferの事でソフトウェアアーキテクチャのスタイルのひとつ。どんなスタイルなのか?については一般的に以下の4原則と言われるものを満たしているものと考えられているようである。
※「ようである」というのは、あくまでスタイルなのでAPIの仕様・設計時の約束事のようなもの(ガイドライン)の細かい部分まで決められていないようで、多種多様なものがRESTと呼ばれているため、このような表現をした。

(1)ステートレスなクライアント/サーバプロトコル
(2)すべての情報(リソース)に適用できる「よく定義された操作」のセット
(3)リソースを一意に識別する「汎用的な構文」
(4)アプリケーションの情報と状態遷移の両方を扱うことができる「ハイパーメディアの使用」

※上記の4原則だが、原著論文で述べられている6つの性質を整理したものではないか、という見解があるなど、原則という呼び名の割には出典は明らかでないようである(本当にRESTは「4つの原則」?原文に当たって分かった驚きの事実)。

※RESTの意味する事はまちまちとはいえ、Richardson Maturity Modelで言われているようなレベルにおいて、最終的にはレベル3を目指しましょうという事になっているようである。

REST APIとは

RESTのスタイルで設計・実装されたAPIの事。 RESTがスタイルという事もあり、どういうものをREST APIと考えればいいのか?という話になるが、その1つの答えとしては以下の6つの性質を満たしているAPIをREST APIと呼ぶというものになる。

(1)クライアント、サーバー、リソースからなるクライアント/サーバーアーキテクチャで、要求は HTTP 経由で管理される
(2)ステートレス・クライアントサーバー通信。get 要求の間にクライアント情報は格納されず、各要求は独立しており、分離している
(3)クライアントとサーバーのやり取りを効率化する、キャッシュ可能なデータ
(4)標準化された形式で情報を転送するための、コンポーネント間で統一されたインタフェース
(5)要求された情報の取得に関係する、各タイプのサーバー (セキュリティ、負荷分散などを実行) を、クライアントからは参照できない階層に整理する階層化システム
(6)コードオンデマンド (任意):実行可能コードを要求されたときにサーバーからクライアントに送信して、クライアントの機能を拡張する機能

※私は上記の性質を見てもイメージがしにくかったため、REST APIがどんなものか?をもう少し具体的な言葉で書いてみると、

・HTTPで通信(情報の操作(取得、作成、更新、削除)は全てHTTPメソッド(GET、POST、PUT、DELETE)で行う)
・URLのパスでどんな操作か分かる(例えば、パラメータ(情報)はパスパラメータ・クエリーパラメータで送るのが望ましい。bodyでパラメータを送るというのはパスを見てもわからないので理想ではない)
・レスポンスはJSON

と言った要素を持つAPIをイメージすると良いと感じた。ただ、やはり厳密な定義は存在していないようなので大まかな枠組や設計原則として理解しておくのが良さそう。

・参考:Introduction
・参考:オープンAPIとOpen APIととWeb APIとREST APIの違い。
・参考:RESTとは何か。
・参考:RESTful APIとは何なのか
・参考:REST API とは
・参考:Representational State Transfer

実際にREST APIを設計してみる

ツールとドキュメント

OpenAPI Specificationに沿って設計していく。

設計に使うツールはStoplight Studioを使う。

・参考:本当に使ってよかったOpenAPI (Swagger) ツール

設計したREST API

Todoを登録したり、編集したり、削除したり、一括で取得したりできるAPI(長いので一部抜粋している)。

openapi: 3.0.0
info:
  title: todo-api
  version: '1.0'
  # 省略
servers:
  - url: 'http://localhost:3000'
paths:
  '/api/todo/{id}':
    description: 'description of /api/todo/{id}'
    parameters:
      - schema:
          type: integer
          minimum: 1
          example: 1
        name: id
        in: path
        description: Id of an existing todo.
        required: true
    get:
      summary: Get Todo Info by Todo ID
      tags: []
      operationId: get-todo-id
      description: Retrieve the information of the todo with the matching todo ID.
      responses:
        '200':
          description: Todo Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToDo'
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Todo Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      # 省略
    patch:
      # 省略
  /api/todo:
    post:
      summary: Create New Todo
      operationId: post-todo
      description: Todoを新規で作成する
      tags: []
      responses:
        '201':
          description: Todo Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToDo'
        '400':
          description: Missing Required Information(Bad Request)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Todo Already Exits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                title:
                  type: string
                  description: Todoのタイトル
                  example: Make French Toast
                  minLength: 1
                  maxLength: 128
                description:
                  type: string
                  example: 美味しいフレンチトースト作ってね
                  maxLength: 255
                assigin_person:
                  type: string
                  example: 山田 太郎
                  maxLength: 20
                  minLength: 1
              required:
                - title
                - assigin_person
        description: APIに必要なフィールドを投稿して、新しいToDoを作成します
  /api/todos:
    get:
      summary: Get All Todo
      operationId: get-todos
      description: |-
        すべてのToDoを取得する
        ただし、ToDoの総数が20件を超えると、それ以上のToDoは返却されず、代わりに続きのToDo開始位置を示すlast_indexが返されます
      tags: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                  todos:
                    type: array
                    items:
                      $ref: '#/components/schemas/ToDo'
                  last_id:
                    type: integer
                required:
                  - total
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Todo Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    ToDo:
      type: object
      title: ToDo
      description: Todoオブジェクトの共通スキーマ
      properties:
        id:
          type: integer
          description: ユニークID
          example: 1
          readOnly: true
        title:
          type: string
          description: Todoのタイトル
          example: Buy Book
          maxLength: 225
          minLength: 1
        description:
          type: string
          description: Todoの説明
          example: 本を買ってきてください
          maxLength: 255
          minLength: 1
        is_complete:
          type: boolean
          description: Todoの完了・未完了を示すフラグ(trueが完了)
          example: false
        assagin_person:
          type: string
          description: Todoを担当する人の名前
          example: yuta katayama
          maxLength: 20
          minLength: 1
          nullable: true
        created_at:
          type: integer
          format: int64
          description: Todoが作成された時刻(UNIXタイムで単位は秒)
          example: 1633480000
          readOnly: true
        updated_at:
          type: integer
          format: int64
          description: Todoが更新された時刻(UNIXタイムで単位は秒)
          example: 1633480000
          readOnly: true
      required:
        - id
        - title
        - created_at
    Error:
      type: object
      title: Error
      description: エラーオブジェクトの共通スキーマ
      properties:
        state:
          type: string
          description: エラーの種別
          example: TodoNotFound
          maxLength: 255
        message:
          type: string
          description: エラーメッセージ
          maxLength: 255
          example: Todoが1件も存在しません
          readOnly: true
      required:
        - state
        - message

●info

APIについての概要説明を定義できる項目(連絡先とかバージョンとか、APIの説明とかを記載する)
https://swagger.io/specification/#info-object

●servers

エンドポイントとなるサーバ情報を定義できる項目(APIのホストとなるURL)
https://swagger.io/specification/#server-object

●paths

各エンドポイントを相対パスで定義する事ができる項目で、ここはserversのパスに続く部分
https://swagger.io/specification/#paths-object

●/{path}('/api/todo/:id'とか)

パスで使用できる操作(GET, POSTとか)を定義できる項目(各URLパスごとにGET, POSTとかの操作があるがそれを定義する) https://swagger.io/specification/#path-item-object

●parameters

操作に関わるパラメーター(パスパラメータ、クエリーパラメータなど)を定義できる項目(例えばexpress-openapi-validator等を使って、requestのバリデーションチェックでも使われる情報なのでここを適切に定義するのが重要)
https://swagger.io/specification/#parameter-object

●{operation}(get, postなど)

pathsに定義したパスに対するAPI操作を定義できる項目
https://swagger.io/specification/#operation-object

●responses

{operations}で定義した操作のレスポンス(200, 404などのHTTPレスポンスコード)を定義できる項目
https://swagger.io/specification/#responses-object

●{HTTP status code}(200, 404など)

各HTTPステータスコード毎にそのレスポンスの内容を定義できる項目
https://swagger.io/specification/#response-object

●content

HTTPレスポンスの具体的な中身(どのメディアタイプか?)を定義できる項目
https://swagger.io/specification/#media-type-object

●{media type object}(application/jsonなど)

各メディアタイプに応じたレスポンスのスキーマ(どんなキーを持つか?)やその例などを定義できる項目(ここで定義するレスポンスのスキーマは、例えばexpress-openapi-validator等を使って、responseのバリデーションチェックでも使われる情報なのでここを適切に定義するのが重要)
https://swagger.io/specification/#media-type-object

●components

色々なところで再利用可能なオブジェクトのセット(スキーマ、レスポンス、リクエストなどのまとまり・共通化したもの)を定義できる項目
https://swagger.io/specification/#components-object

●schemas

レスポンス・リクエストに設定可能なスキーマ(複数も可)を定義できる項目(ここにスキーマを定義しておき、実際の各APIの操作の定義時に$refで参照するようにして設定できるので、再利用可能という理屈。エラーのレスポンス等はAPIで共通化させたいとかの時に便利。)
https://swagger.io/specification/#schema-object

APIの設計時のレビューで直した部分

APIの設計というものを全くやったことがなかったので非常に基本的な事から指摘を頂いた。自身に定着化させる意味でもその指摘を書き残しておこうと思う。

・プロパティ名はスネークケース(snake_case)で書く

・複数形は複数のデータのみ(1つのデータしか取得できない仕様なのに、/api/todos/:idというような定義をしない)
 ※これについては正しい・正しくないの話ではなく、決めの問題で今回はこうするという類の話なので組織によっては違うこともある

・時刻についてはAPIでは、タイムゾーンが不明になるのでUNIXタイムでやるといい(typeはint64にすると2038年問題も乗り越えられる)
 ※stringにしてタイムゾーンを付けて返すこともできるがなかなか大変
 ※経過時間の単位がms(ミリ秒)等色々な単位で定義できるが、基本的なAPIであればs(秒)で十分(int64だと単位までは分からないので説明(description)に秒とか書くのが丁寧

・404等でもHTTPステータスだけ返すのではなく、responseのスキーマを定義してJSONを返すべき

・409は重複エラーの事で、同じIDで登録不可な場面等のエラーで返すHTTPステータス(一意制約エラー)

・PUT・PATCHの違いは、会社や組織でそれぞれで違う定義をすることがあるので何を意味しているか?は要確認(ちなみに今回の設計では、PUT:全更新、PATCH:部分更新と定義した)

・additionalProperties: falseにすることで、スキーマに定義されたプロパティ以外のプロパティが含まれている時にエラーになる(バリデーションの実装でこれが効いてくる)

・deleteが成功した後のHTTPステータスは204を返す(204 No Content

・プロパティの設定で、文字数制限の設定・デフォルト値の設定などができるが、PATCH(部分更新)の際に意図せずDBに値が入ったりしてしまう事があるのでデフォルト値を設定する際には注意が必要

・Stoplight Studioで設計をしている場合、プロパティのenumを設定しさらにバリデーション(最大文字数とか)の設定もした時に、enumの値がバリデーションのルールにひっかかる事がないようにする(仮にバリデーションのルールに引っかかっていてもツールは何も教えてくれない・・・)

・allOfで再利用する時に、allOf元でadditionalProperties: falseをつけると、再利用先でプロパティ追加に対して怒られるので注意

・プロパティのnull許可をする際はnullable: trueとする
 ※ツールでStoplight Studioを使っている場合、ツール側でnull許容を設定すると以下のようになってしまう(ツールのバージョンによるかもしれず、再現しないかもしれません)ので、yamlを自分で修正する

type:
  - string
  - 'null'

・一覧を返却するAPIでは、以下のようなスキーマ(トップレベルのプロパティはtodosでarrayのようにする)

・response(error response)は、components>schemasで定義した使いまわせる定義を参照するようにすると楽

・stringはDBの最大サイズに応じて最短長と最長長を決める

・Stoplight Studioで自動生成したyamlはresponseがrequestBodyの上にくるようなので(これもバージョンによるかもしれません)、読みやすいようrequestBodyの下に移動する(のが読みやすくなるかな)

APIの設計時に知った事

・OpenAPIのyamlをドキュメントにしてくれるツールがある
 https://github.com/Redocly/redoc

まとめとして

OpenAPIで設計するREST APIについて、実際には明確にこういうルールに基づいて設計すべきという厳格な仕様がないといった事、あくまでスタイルで大まかな枠組や設計原則として捉えるようにするのが良いという事、など理解を深められた。 今後も、今回理解した事にプラスしてさらに理解を深めて適切なAPIを設計できるよう追加で学習していきたい。

おまけ(設計したREST APIのサマリの概要)

__________________________________

執筆者プロフィール:Katayama Yuta
SaaS ERPパッケージベンダーにて開発を2年経験。 SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。 最近開発部門へ異動し、再び開発エンジニアに。座学で読み物を読むより、色々手を動かして試したり学んだりするのが好きなタイプ。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/