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のサマリの概要)
__________________________________
お問合せはお気軽に
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/