はじめまして。TIG DXユニット 1の亀井です。
はじめに
みなさん、Swagger使ってますか?
Swaggerや周辺ツールについては 某先輩の記事 にて丁寧に解説されていますので、
本記事では実際にSwaggerのスキーマ定義を設計していく上で取り決めた規約について書いてみたいと思います。
前提
私が在籍しているプロジェクトでは、REST APIは golang でフロントエンドを Vue.js + TypeScript で構築しています。
短期間・高品質での構築を実現するためにSwaggerを設計ドキュメントとしてだけではなく、コード自動生成やモックサーバーに活用させることで徹底したスキーマファーストな開発を行ってきました。
というわけで、今回は下記のツールを利用することを前提として規約を作成しています。
- go-swagger: Goアプリケーションのハンドラ、リクエスト/レスポンスのドメインモデル、バリデーションフレームワーク 2
- openapi-generator/typescript-axios: フロントエンドのHTTPクライアント、リクエスト/レスポンスのインタフェース
- Prism: フロントエンド開発時に利用するモックサーバー
設計規約
バージョン
- OpenAPI v2
- 前述の
go-swaggerが3系に対応されていないため2系を利用
- 前述の
paths
tags
- 必須
- 1URIで1つのタグのみ定義する
- リソース名を単数形で記載する
- キャメルケース
# good |
operationId
- 必須
${HTTPメソッド}${機能物理名}を記載する- キャメルケース
|
summary
- 必須
${機能ID} ${機能論理名}で定義する
summary: XXX-0001 商品参照 |
security
- 必須
- 認証の要否で以下のように定義する
# 認証なし |
- 参考: securityDefinitions
# OAuth認証 |
description
- 必須
- APIの機能概要を記載する。
description: IDを指定して商品情報を取得する。 |
parameters
GET/DELETE API の場合
- in: PATHパラメータ
in: pathまたはクエリパラメータin: queryのみ利用可能 - description: 必須
- name: 物理名を定義する
- 命名規約
- スネークケース
- 原則略語は禁止
type: arrayの場合、xxx_listやxxx_arrayはNGとするtype: booleanの場合、is_xxxやhas_xxxで定義しxxx_flagは非推奨とする
- 命名規約
# good |
POST/PUT API の場合
- in: リクエストボディ
in: bodyのみ利用可能 - name: 全て
name: bodyとする - required: リクエストボディが必須でない場合を除いて
required: trueを定義する - schema: リクエストモデルを
type: objectで定義する
# good |
バリデーション
必須
- required: 必須パラメータのみ
required: trueを定義する - default: 必須でないパラメータでもデフォルト値がある場合は定義する
- required: 必須パラメータのみ
型
- type: 必須
- 文字列:string
- 数値:number
- 整数値:integer
- ブール値:boolean
- 配列:array
- オブジェクト:object
type: arrayの場合、配列要素itemsのtypeも必須type: nullは原則として利用しない- 複数のタイプを定義しない
- type: 必須
桁
- 文字列
- 最大桁数:
maxLength - 最小桁数:
minLength
- 最大桁数:
- 数値または整数値
- 最大値(境界値を含む):
maximum - 最小値(境界値を含む):
maximum - 境界値を含まない場合のみ
exclusiveMinimum: trueまたはexclusiveMaximum: trueを定義
- 最大値(境界値を含む):
- 配列:
- 最大要素数:
maxItems - 最小要素数:
minItems required: trueの場合は原則としてminItems: 1を定義する
- 最大要素数:
- 文字列
区分値
enum必須descriptionに区分値の論理名を記載する# ex. enum
name: gender
type: string
enum:
- '00'
- '01'
- '02'
description: |
性別
00: 不明
01: 男
02: 女
日付/日時/時刻
日付
ISO 8601拡張形式(YYYY-MM-DD)とする
example:
2020-01-31name: 接尾辞
_datetype:
stringformat:
datecreated_date:
type: string
example: '2020-01-31'
format: date
日時
タイムゾーン指定子付きISO 8601形式とする
秒精度(YYYY-MM-DDThh:mm:ss+TZD)の場合
example:
2020-01-31T23:59:59+09:00name: 接尾辞
_date_timetype:
stringformat:
date-timecreated_date_time:
type: string
example: '2020-01-31T23:59:59+09:00'
format: date-time
ミリ秒精度(YYYY-MM-DDThh:mm:ss.sss+TZD)の場合
example:
2020-01-31T23:59:59.000+09:00name: 接尾辞
_date_timetype:
stringpattern: 必須
created_date_time:
type: string
example: '2020-01-31T23:59:59.000+09:00'
pattern: '^((?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9][0-9][0-9])[+|-]([0-9][0-9]:[0-9][0-9])$'`
時刻
ISO 8601形式(hh:mm)とする
example:
23:59name: 接尾辞
_timetype: string
pattern: 必須
created_time:
type: string
example: 23:59
pattern: '^(2[0-3]|[01][0-9]):([0-5][0-9])$'
その他
- 正規表現で表現できる文字列は
patternを利用して定義すること
- 正規表現で表現できる文字列は
responses
GET APIの場合
- description
- 必須
- HTTPステータスコードのメッセージを記載すること
- schema
HTTPステータス:200の場合
type: objectでレスポンスモデルを定義する- required: 必須で返る項目を定義する
- 再利用可能なモデルを
definitions配下に定義する- 複合的なモデルを定義する場合は
allOfを利用する
- 複合的なモデルを定義する場合は
# good
getProductsResponse:
allOf:
- type: object
properties:
products:
type: array
items:
$ref: "#/definitions/product"
required:
- products
- $ref: "#/definitions/pagination"
# bad
getProductsResponse:
type: array # TypeScriptのInterfaceが適切に定義されません
items:
product:
type: object
properties:
product_id:
type: string
# required: true を定義しないとundefined許容の変数となり不要なType Guardが必要になる
product_name:
type: stringHTTPステータスコード:400系または500系の場合
- 共通で定義されたレスポンスモデルを利用すること
- examples
- ステータスコード:200の場合のみ
application/jsonという命名で必須 - 必須項目は必ず値を記載すること
- ステータスコード:200の場合のみ
200: |
POST/PUT/DELETE APIの場合
- description
- 必須
- HTTPステータスコードのメッセージを記載すること
- schema
- 原則不要
- 必要な場合は
type: objectでレスポンスモデルを定義する
- examples
- schemaを定義した場合のみ記載する
- ステータスコード:200の場合のみ
application/jsonという命名で必須 - 必須項目は必ず値を記載すること
models
リクエストモデル
- URI単位で1モデルを定義する
- 命名規約
- キャメルケース
postXxxxRequestまたはputXxxxRequest
# POST /products |
レスポンスモデル
- URI単位で1モデルを定義する
- リソースモデルをそのまま利用できる場合は不要
- 命名規約
- キャメルケース
getXxxxResponse
# GET /products |
リソースモデル
- リソースや共通で利用するエンティティの単位で単数形で定義する
- 命名規約
- キャメルケース
pagination: |
HTTPステータス
- 原則としてRFC 7231で定義されているレスポンスステータスコードを利用します
- 以下、設計者が特に意識すべきものを抜粋して記載します。
共通
- バリデーションエラー:
400 Bad Request - 業務エラー:
400 Bad Request - 認証エラー:
401 Unauthorized - 認可エラー:
403 Forbidden - システムエラー:
500 Internal Server Error
GET
- 正常系:
200 OK - 検索系APIで結果0件:
200 OK - キー検索系APIで対象リソースが存在しないエラー:
404 Not Found
POST
- 正常系(同期):
201 Created - 正常系(非同期):
202 Accepted - 一意制約違反エラー:
409 Conflict - 親リソースが存在しないエラー:
404 Not Found
PUT
- 正常系(同期):
200 OK - 正常系(非同期):
202 Accepted - 対象リソースが存在しないエラー:
404 Not Found
DELETE
- 正常系:
204 No Content - 対象リソースが存在しないエラー:
404 Not Found
さいごに
今回はSwaggerやREST APIの設計に慣れてないメンバーを含む複数人で設計していくことを踏まえて、Swaggerに精通している方には自明な内容を含めやや細かめに規約を設定してみました。
結果として品質の高いスキーマ定義でコードを自動生成することでテスト工数も削減できますし、TypeScriptの恩恵をしっかり享受できました。
よいソースコードを書くために正しく美しいスキーマ定義を設計しましょう。
- 1.Technology Innovation Groupの略で、フューチャーの中でも特にIT技術に特化した部隊です。その中でもDXチームは特にデジタルトランスフォーメーションに関わる仕事を推進していくチームです。 ↩
- 2.go-swaggerについては WAFとして go-swagger を選択してみた で詳しく紹介されています。 ↩