先日、OpenAPIファースト開発について一般論を書きました。今回はその具体版です。うちのチームで試している計画ワークフローと、それに添えるYAMLの例を紹介します。
きっかけ#
発端はレトロスペクティブでした。APIの計画がいかに見えていないか、フィードバックは容赦なかった。バックエンドは要件を自分たちなりに解釈してエンドポイントを作り、フロントエンドは後からレスポンスの形を知り、ズレは統合の段階、つまり一番高くつくタイミングで発覚する。そこで出た提案が、両サイドを計画段階から巻き込み、OpenAPIスキーマをすべての土台にしようというものでした。
プロセス#
毎週、バックエンドとフロントエンドから1人ずつ、計2人の代表を選びます。代表の仕事は、スプリントの要件を理解して、計画用のOpenAPIスキーマをそれに合わせることです。2人でスキーマをレビューし、合っていない部分を直していけば、作り始める前に、双方が何を作るのかを正確に把握できます。
修正が落ち着いたら、スキーマをリポジトリにコミットします。するとCI/CDがそのyamlからモックAPIを生成します。スプリントが終わったら、次のスプリントのAPI要件をまとめた新しいyamlを切って、同じサイクルを繰り返します。
ツールまわりのメモ#
- yamlの可視化にはSwagger Editorで十分です。ファイルをアップロードすれば、ちゃんとしたSwaggerドキュメントとして表示されます。
- このスキーマはあくまで計画用の成果物です。本物のAPIがデプロイされたら、各サービスが自分のopenapi.jsonを生成するべきで、それをダウンロードして次の計画に再利用します。
- CI/CDパイプラインには、yamlを受け取ってモックサーバーを立てる仕組みが必要です。openapi-mockは有力な選択肢だと思います。
なぜやる価値があるのか#
議論が一番安い場所に移動するからです。スキーマを書いている段階でフロントとバックのエンジニアが同席していれば、誤解はコードになる前に捕まります。API要件はスプリント途中で再交渉されるのではなく一度で合意されるし、モックサーバーのおかげでフロントエンドは待たずにすぐ着手できる。重要な細部の見落としもぐっと減ります。なにしろ2組の目と、書面になった契約をすり抜けなければならないので。
革命的な話はひとつもありません。ただ、機械がモックとして返せるくらい具体的なところまで計画を落とし込んだだけです。そして結局のところ、計画に必要な具体性とは、ちょうどそのくらいなのだと思います。
付録A - yamlの例#
openapi: 3.1.0
info:
title: PKY Sprint 3
description: |-
PKY Sprint 3 Development Schema
termsOfService:
contact:
email: jared@lynskey.co.nz
license:
name: None
url:
version: 1.0.11
externalDocs:
description:
url:
servers:
- url: https://api-mock.com/api/v3
tags:
- name: curation
description: PKY-1081 Curated Lists Feature
externalDocs:
description: Jira Epic
url: https://pickydev.atlassian.net/browse/PKY-1081
paths:
/curations:
get:
tags:
- curation
summary: Return the curations created by users
description: Returns a map of status codes to quantities
operationId: getCurations
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
security:
- api_key: []
/curations/{curationId}:
get:
tags:
- curation
summary: Find purchase order by ID
description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of order that needs to be fetched
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
application/xml:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Invalid ID supplied
'404':
description: Order not found
delete:
tags:
- curation
summary: Delete purchase order by ID
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid ID supplied
'404':
description: Order not found
components:
schemas:
Curation:
type: object
properties:
id:
type: integer
format: int64
examples: [10]
curationId:
type: integer
format: int64
examples: [198772]
quantity:
type: integer
format: int32
examples: [7]
status:
type: string
description: Order Status
examples: [approved]
enum:
- placed
- approved
- delivered
complete:
type: boolean
xml:
name: order
ApiResponse:
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
xml:
name: '##default'
requestBodies:
Curation:
description: Curation object that needs to be added
content:
application/json:
schema:
$ref:
application/xml:
schema:
$ref:
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl:
scopes:
write:curations: modify curations in your account
read:curations: read your curations
api_key:
type: apiKey
name: api_key
in: header
