OpenAPI や Protocol Buffers のおかげで開発がかなり捗っている話
こんにちは、インキュベーション本部エンジニアの加藤です。 主に CLINICS アプリの開発を担当しています。
はじめに
CLINICS アプリの開発では OpenAPI や gRPC を利用しています。 OpenAPI と gRPC の間には何の関係もないのですが、どちらも API の仕様をスキーマ言語で記述するという点では共通しています。 今回はこの API スキーマが開発にもたらすメリットについて紹介していこうと思います。
API ドキュメントとしてのスキーマ定義
既存のコードに機能を追加する際や修正を加える際に気にすることの多い部分は API の仕様ではないかと思います。 「リクエストやレスポンスはどのようなデータなのか」「この値は必須なのか、任意なのか」「データの型は数値なのか、文字列なのか」「フォーマットは決まっているのか」など、多くのことを把握する必要があります。
しかし、こういった API 仕様のドキュメントが整備されていないということも多いのではないでしょうか。 また、ドキュメントはあるけれどリクエストとレスポンスのサンプル JSON が貼ってあるだけだったり、時間が経つうちに実装とドキュメントが乖離してしまいアテにならなくなってしまっている場合もあります。 参考にできるドキュメントがない場合は、直接バックエンドの実装を見に行くこともあるのですが、目的のコードを探し出して読み解くのは意外と時間がかかります。 特に自分以外のエンジニアが実装した機能の場合、API の仕様を実装から紐解くのはかなり骨が折れる作業です。
CLINICS アプリの開発ではこういった手間を減らすために OpenAPI や gRPC を利用しています。 REST API のスキーマは OpenAPI を使って記述しています。 gRPC を利用している部分は Protocol Buffers で RPC のインターフェースが記述されるため、これが API のスキーマになっています。 OpenAPI や Protocol Buffers の定義を参照することで API の仕様が容易に把握できるため、非常に便利です。
OpenAPI や Protocol Buffers などのスキーマ言語の良い点は、宣言的な DSL でスキーマを定義できる点です。 実装言語に依存しない形での記述ができ、自然言語のような曖昧さもないためより正確に API 仕様を記述することができます。
ここからは OpenAPI や Protocol Buffers の実際に記述例を挙げながら、どういった形でスキーマを記述していくのか簡単に見ていきたいと思います。
OpenAPI
OpenAPI は REST API のインターフェースを記述する仕様です。 エンドポイントのリクエストパラメータやレスポンスの構造などを JSON や YAML で記述していくことができます。 かつては Swagger という名称だったため、そちらの名前で知っているという方もいらっしゃるかもしれません。
実際の OpenAPI の定義は以下のような形になります。
openapi: 3.0.2
info: { title: OpenAPI Sample, version: 1.0.0 }
paths:
"/users/{user_id}":
get:
parameters:
- name: user_id
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
id: { type: string }
name: { type: string }
age: { type: integer, nullable: true }
required: [id, name, age]
"404":
description: Not Found
"500":
description: Internal Server Error
例としてユーザー ID からユーザーのデータを取得する API を記述してみました。 上記の定義を見ると、
GET: /users/{user_id}
というエンドポイントがある- ユーザーの ID (
user_id
) を string でパラメータとしてパスの中に埋め込む - レスポンスは 200, 404, 500 が返ってくる可能性がある
といったことが読み取れるかと思います。 また、レスポンスは以下のような JSON が返ってくることもスキーマから読み取ることができます。
{
"id": "12345678-1234-1234-1234-123456789abc",
"name": "user name",
"age": 25
}
ここでは OpenAPI の記述の仕方についてあまり深く触れませんが、この他にもデータのフォーマットや省略された場合のデフォルト値などの記述なども可能です。 より詳しくはこちらを参考にしてみてください。 これらをうまく使うことでより正確に API の仕様を記述することができます。
Protocol Buffers
Protocol Buffers は主に gRPC で用いられるデータのシリアライゼーション形式です。
専用のスキーマ言語を用いてデータの構造や RPC のメソッドの定義を .proto
ファイルに記述していきます。
実際の .proto
ファイルは以下のような形になります。
syntax = "proto3";
package example.protobuf;
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
User user = 1;
}
message User {
string id = 1;
string name = 2;
int32 age = 3;
}
少し独特な見た目をしていますが、message
として構造体のようなデータ構造が定義されていることが見て取れると思います。
各メッセージにはフィールドが定義されており、型とフィールド名が宣言されています。
スキーマ言語を使うメリット
スキーマはドキュメントの代わりとして使えるだけでなく、他にもいくつか使い道があるので紹介していこうと思います。
スキーマからソースコードを自動生成できる
スキーマ言語による記述は人間にとって読みやすいだけでなく機械的に処理できる形式でもあるので、スキーマからソースコードを自動生成することも可能です。 プログラミング言語や使っているフレームワークによらず、サーバー側ではリクエストのバリデーションを、またクライアント側ではレスポンスの JSON をクラスや構造体に詰めるような処理を書くことが多いのではないでしょうか。 事前にスキーマ定義を書いている場合はスキーマを見ながらこういったコードを書き起こしていくことになるのですが、せっかく machine-readable な形のスキーマがあるのだから自動でコードを生成したくなるのが自然な考えでしょう。 自動で生成してしまえば定型のコードを書く手間を減らせるだけでなく、手書きするとどうしても起こしがちな JSON のキー名を間違えるといったミスを防ぐこともできます。 また、Web, iOS, Android などの複数プラットフォームでサービスを展開している場合でも、それぞれの実装が乖離しないように管理していくことも容易です。
gRPC の場合は Protocol Buffers からソースコードを生成するのがほぼ前提となっています。
protoc コマンドを利用すると .proto
ファイルから各言語のサーバー・クライアントコードを生成することができます。
OpenAPI の場合、openapi-generator や swagger-codegen などのツールを利用することで各種言語のコードを自動生成が可能です。 openapi-generator や swagger-codegen には多くの言語とフレームワークのサーバースタブと API クライアントのジェネレータが用意されています。
既存のジェネレータを使うだけでも十分に強力なのですが、さらに自前でコードジェネレータを書けばプロジェクト固有のルールでコードを生成するといったことも可能です。 OpenAPI のドキュメント自体はただの YAML/JSON なので、パースしてスキーマ定義を再帰的に読んでいけばコード生成に必要な情報を集めることができます。 Protocol Buffers からコードを生成する場合は protoc のプラグインを書くのが簡単でオススメです。
モックサーバーを使ってクライアント開発を進められる
コードの自動生成の他にもスキーマからモックサーバを立ち上げるといった活用方法もあります。 Prism のようなスキーマ定義から固定のデータを返すモックサーバを立ち上げるツールを利用すれば、クライアント側の開発もしやすくなります。 特にフロントエンドとバックエンドで担当が分かれているような場合は、開発初期はモックサーバーを使って開発を始め、バックエンドの実装ができてきた頃に結合するという流れを踏むことで、フロントエンドもより早いタイミングから開発に着手することができるようになります。
実際に Prism を使ってモックサーバーを立ち上げるとこんな感じになります。
スキーマファースト開発
このような開発スタイルはスキーマファースト開発・スキーマ駆動開発などと呼ばれています。 明確な定義や出典があるわけではないのですが、スキーマファーストな開発は以下のような開発スタイル全般を指しています。
- スキーマは実装言語によらない machine-readable な形で記述する
- ドキュメントをスキーマから自動生成する
- クライアント/サーバーのコードをスキーマから自動生成する
実際にスキーマファーストな開発を実践するためにはスキーマをメンテナンスするモチベーションを高く保つ必要があります。 コードも書いた上でスキーマも書かないいけないとなるとおそらくスキーマが更新されなくなっていくので、可能な限りスキーマからコードを生成する努力が必要になります。 一方、うまく実践すればスキーマという形で 100% 信頼できる API ドキュメントが手に入り、クライアント/サーバー間での齟齬も減らせるので、実践してみる価値はあるのではないかと思います。
まとめ
今回は CLINICS アプリの開発でのスキーマ言語の活用例について紹介しました。 実際に開発の中でスキーマ言語を使うことで API 仕様について把握する労力が減り、エンジニア間のコミュニケーションも取りやすくなったと感じています。 また、一部のクライアントコードは OpenAPI や Protocol Buffers から自動生成しているのですが、かなり手間が省けると同時に人間の手で書くと起こりがちなミスも防ぐことができています。 OpenAPI であれば既存の REST API の仕様を書き起こすところから使い始められるので、興味のある方はぜひ一度使ってみてはいかがでしょうか。
最後に
インキュベーション本部では「医療ヘルスケアの未来」をつくる新規事業の立ち上げに挑戦しています。そんな私たちと一緒に働いてみたいと思った方は、ぜひ以下の採用情報もチェックしてみてください。