こんにちは。システム開発一部の吉田です。
普段バックエンドとクラウドしか触っていないのですが、たまたまVueを使ったフロントエンドを担当することになりました。
CSSをまともに勉強したことが無く、めちゃくちゃ苦手なので悪戦苦闘中です。
今回フロントエンドを開発するにあたってローカル環境でAPI叩きながらコード書いていくという手法を取りたいなと思っていて、モックサーバーが欲しくなったのでPrismとCaddyを組み合わせたモックサーバーを導入してみました。
OpenAPI定義が既に作成済みだったのもあり、導入はサクッとできました。その結果、ローカルでの検証がしやすくなり、開発効率が向上したと感じています。おすすめなので紹介させてください。
Prism とは
PrismはOpenAPI定義に基づいてAPIのモックサーバーを作成してくれるパッケージです。
API定義に基づいたリクエストのバリデーション、Examplesを返すレスポンス、Responseのスキーマから動的生成した値を返す...などモックサーバーとしての機能が充実しています。動的生成値については後続で説明します。
例によってDockerイメージが配布されているのでこれを使っていきましょう。
Prismを触ってみる
この記事ではOpenAPIの定義についてはサンプルを使っていきます。早速docker-compose.yamlを作って起動します。
version: '3' services: prism: image: stoplight/prism:4 network_mode: host command: > mock -p 4010 --host 0.0.0.0 https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml
起動できたらlocahost:4010/pets
にリクエストを送ってみると...こんな感じでschemasで定義された通りのレスポンスが返ってきます。
OpenAPIの定義からサクッとモックサーバーを作成することが出来ました。しかし、API定義によるとGET /pets
はペットのリストを返すパスなので、データが1つしか返ってこないのはちょっと困ります。
そこで、Schema定義に沿って動的生成した値をレスポンスとして返すように設定します。mockのコマンドにdオプションを指定するだけ。
mock -p 4010 --host 0.0.0.0
→ mock -d -p 4010 --host 0.0.0.0
このときのレスポンスは以下の通り。idに数値、nameとtagにはLorem ipsumっぽい文字列が入っているデータのリストが返ってきました。リクエストを送るたびにレスポンスの中身は変わります。
動的に生成される値はschemasを元に決まるので、schemasの中でformatなどを指定するとより想定に近い値が返ってきます。
先ほどのAPI定義にちょっとだけ手を加えて生成値の変化を見てみましょう。
docker-compose.yamlと同じディレクトリにpetstore.ymlとして配置します。
長いので折り畳み
openapi: "3.0.0" info: version: 1.0.0 title: Swagger Petstore license: name: MIT servers: - url: http://petstore.swagger.io/v1 paths: /pets: get: summary: List all pets operationId: listPets tags: - pets parameters: - name: limit in: query description: How many items to return at one time (max 100) required: false schema: type: integer maximum: 100 format: int32 responses: '200': description: A paged array of pets headers: x-next: description: A link to the next page of responses schema: type: string content: application/json: schema: $ref: "#/components/schemas/Pets" examples: example1: $ref: '#/components/examples/get-pets-1' example2: $ref: '#/components/examples/get-pets-2' default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" post: summary: Create a pet operationId: createPets tags: - pets responses: '201': description: Null response default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" /pets/{petId}: get: summary: Info for a specific pet operationId: showPetById tags: - pets parameters: - name: petId in: path required: true description: The id of the pet to retrieve schema: type: string responses: '200': description: Expected response to a valid request content: application/json: schema: $ref: "#/components/schemas/Pet" examples: example1: $ref: '#/components/examples/get-pets-petid-1' default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" components: schemas: Pet: type: object required: - id - name properties: id: type: integer format: int64 minimum: 1 maximum: 100 name: type: string pattern: '^([a-z]{1,10})$' tag: type: string enum: - cat - dog - hamster - rabbit mailAddress: type: string format: email Pets: type: array maxItems: 100 items: $ref: "#/components/schemas/Pet" Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string examples: get-pets-1: value: - id: 1 name: max tag: dog email: example1@example.com - id: 2 name: charlie tag: rabbit email: example2@example.com - id: 3 name: bella tag: hamster email: example3@example.com - id: 4 name: lily tag: cat email: example4@example.com get-pets-2: value: - id: 5 name: kitty tag: cat email: example5@example.com - id: 6 name: daisy tag: rabbit email: example6@example.com - id: 7 name: sasha tag: cat email: example7example.com get-pets-petid-1: value: id: 1 name: kitty tag: cat email: example1@example.com
変更内容
- idに最大値と最小値を設定
- nameに正規表現を適用
- tagにenumを設定
format: email
のmailAddressのSchema追加- GET /petsのレスポンスにexampleを2つ追加
- GET /pets/{petid}のレスポンスにexampleを1つ追加
docker-compose.yamlも合わせて書き替えて、ローカルの定義ファイルを読み込むようにします。
version: '3' services: prism: image: stoplight/prism:4 network_mode: host volumes: - ./petstore.yaml:/tmp/petstore.yaml command: mock -d -p 4010 --host 0.0.0.0 /tmp/petstore.yaml
これで起動して先ほどと同じパスにリクエストを送ってみると...変更前のレスポンスと違って、id, name, tagが規定された通りに返って来ているのが分かると思います。
追加したmailAddressもメールアドレスっぽいフォーマットで返って来ていますね。このように、OpenAPI定義のSchemaを詳細化するとより効果的にPrismを利用できます。
また、↑ような動的生成の値ではなく、exampleで設定した値がレスポンスとして欲しいときがあると思います。その際はdオプションを外す、あるいはリクエストヘッダーにPrefer: example=example2
のような形で欲しいexample名を指定してあげます。exampleが定義済みかつdオプションを外したときは一番上に定義されたexampleがレスポンスになります。
Preferについては公式のドキュメントで言及されています。詳細は以下を参照してください。
補足ですが、dオプションを指定していた場合でもPreferの指定が優先される仕様のようです。(dオプション指定時にPrefer: example=example2
を設定した場合、example2がレスポンスとして返ってきます。)
そのほかに、Perefer: dynamic=true
で動的生成値を返すようにしたり、Prefer: code=400
でエラーの場合のレスポンスを返すように設定することもできます。便利。
とはいえ、リクエストヘッダーをいちいち意識しないといけないのは正直めんどくさいです。axiosでAPIコールをする場合、ヘッダーを追加するにはリクエスト部分の実装を書き換えないといけません。
動的生成値とexample1つを返したいのであればdocker-composeでdオプション付きとそうでないものの2つのコンテナをポートを分けて使えばいいのですが、Exampleの2つ目を返したいといった場合はやはりヘッダーを編集する必要があります。
Prismだけはどうにもならないので、Caddyというリバースプロキシを組み合わせることでこのヘッダー問題を解決することにしました。
CaddyとPrismを組みあわせる
Caddyの説明をすると長くなってしまうので本記事では省きます。超ざっくりした説明をするとWebサーバとリバースプロキシとロードバランサーの機能を持ったソフトウェアです。個人的な感想ですが、リバースプロキシとしてはSquidより使い方がわかりやすくて良いです。
Prismと同じく公式のドキュメントが充実しているので詳細はそちらを参照してください。
まずはdocker-composeから書き換えていきます。ここではproxyというコンテナを追加で立てています。ついでにポートも変更。2つのコンテナを同じdocker network上に乗せて疎通させます。
version: '3' services: prism: image: stoplight/prism:4 volumes: - ./petstore.yaml:/tmp/petstore.yaml command: mock -d -p 4010 --host 0.0.0.0 /tmp/petstore.yaml ports: - 8081:4010 proxy: image: caddy volumes: - ./Caddyfile:/etc/caddy/Caddyfile ports: - '8080:80' depends_on: - prism
Caddyfileという名前のファイルでcaddyの振る舞いを定義します。docker-compose.yamlと同じディレクトリに以下で作成します。
http://localhost { handle_path /dynamic/* { request_header +Prefer dynamic=true reverse_proxy prism:4010 } handle_path /example1/* { request_header +Prefer example=example1 reverse_proxy prism:4010 } handle_path /example2/* { uri strip_prefix /example2 request_header +Prefer example=example2 reverse_proxy prism:4010 } handle_path /400/* { request_header +Prefer code=400 reverse_proxy prism:4010 } }
記述内容からもなんとなくやりたいことが見えてくると思います。Caddyをリバースプロキシとして、パスごとにPreferのリクエストヘッダーを付与して振り分ける方式としました。
この時返ってくるレスポンスは以下の通り。
GET localhost:8080/dynamic/pets
→ 動的な値GET localhost:8080/example1/pets
→ exapmle1の値GET localhost:8080/example2/pets
→ exapmle2の値GET localhost:8080/400/pets
→ 400エラー(今回のAPI定義だと、defaultとして定義しているレスポンス)
パスで振り分けが可能なのでAPIコール先のURLを環境変数として持てば、axiosの実装に手を加えることなく検証を進めることが可能です。とても使いやすくなりました。
さらなる応用
ここまでの状態でも実用で耐えうる形にはなっていると思います。あくまでローカルで使うためのモックですし。
しかし、私の場合、/dynamicをアクセス先としているときにログインで使っているAPIパスのレスポンスも動的な値で返って来てしまってログインの検証が上手く行かないという壁に当たりました。SPAでログイン機能を先に作ったのでSPAをローカルで動かした際にログインが出来ないとその先の検証が上手く行かない状態になってしまった...という流れです。
表現が分かりにくくてすみません。噛み砕くと、/dynamic、/400にアクセスした時でも一部のパス、一部のメソッドだけはexampleの値が返ってくるようにしないといけませんでした。そのためにCaddyfileにさらに手を加えることにしました。
今回のAPI定義におけるCaddyfileの設定例を記載します。GET /pets/{petId}
をexample1のレスポンスにするという変更内容になります。
http://localhost { @get_pets_petid_path { path */pets/* method GET } route @get_pets_petid_path { rewrite * /pets/1 request_header +Prefer example=example1 reverse_proxy prism:4010 } route * { handle_path /dynamic/* { request_header +Prefer dynamic=true reverse_proxy prism:4010 } handle_path /example1/* { request_header +Prefer example=example1 reverse_proxy prism:4010 } handle_path /example2/* { request_header +Prefer example=example2 reverse_proxy prism:4010 } handle_path /400/* { request_header +Prefer code=400 reverse_proxy prism:4010 } } }
この状態でcomposeを起動してGET localhost:8080/dynamic/pets/{petId}
にリクエストを送ってみると...example1で指定した値が返ってきます。
GET localhost:8080/dynamic/pets
のレスポンスは動的生成値のままです。
このようにCaddyfile内部でRouteを分けて設定することによってハンドルするパスの評価順を任意に設定することが出来ます。
(通常上からの評価になるのですが、変数で宣言しているパスマッチャーだけ上からの評価になりませんでした。)
まとめ
今回はPrismの説明とPrism単体だと使いにくい部分をCaddyでカバーする方法をご紹介しました。いかがだったでしょうか?
Prism単体の紹介記事は多いです。が、Caddyと組み合わせて紹介しているところが少なかったので取り上げてみました。
OpenAPIといった標準的なスキーマを元に開発を進める手法のことをスキーマ駆動開発というらしいですね。調べている途中に見つけました。(詳しくはスキーマ駆動開発でググってみて欲しいです。)
使いやすいモックがあれば実際にAPIを叩きつつコードを書けるので、今回のモックサーバーはスキーマ駆動開発を促進させるツールとして適切なのではないでしょうか。ぜひ使ってみてください。