PreferヘッダーでAPIモックサーバーを制御する


皆様、プロジェクトでOpenAPIは活用されていますか?

OpenAPIは、APIの設計や実装に関する仕様を YAML や JSON で定義することのできるフォーマットです。例えばどのURLにどのようなリクエストを送るとどのようなレスポンスが返されるか、といった定義を開発メンバー間で共有することができます。

Gaji-Labo はフロントエンドやUIを得意とする会社ですが、フロントエンドだけ出来れば良いということではなく、専門技術を通じてプロダクトチームの状態が良くなることや成長に貢献することを価値と考えて仕事をしています。

アプリケーション開発はフロントエンドだけでは完結せず、バックエンドとの連携が必須となりますが、異なるフィールドで活躍するフロントエンドとバックエンドが円滑にコミュニケーションをとるためには共通言語が必要です。OpenAPIは、まさにそのような場で真価を発揮する強力なコミュニケーションツールです。

このエントリーでは、OpenAPI を活用するためのちょっとしたアイデアを紹介いたします。

OpenAPI、そして Prism について

OpenAPIで設計したAPIを開発用のモックサーバーで起動したい。それを容易に実現してくれるのが、Prismです。

PrismのモックサーバーはOpenAPIドキュメントを読み込み、そこに定義されている通りの仕様で擬似的なAPIサーバーを起動してくれます。PrismはOpenAPIドキュメント内の example でレスポンスデータを定義できるほか、設計仕様に則っていない誤ったリクエストが送信された時にバリデーションをしてエラーを返して教えてくれます。

フロントエンドはローカルで立ち上げたモックサーバーと通信しながら開発を進め、一方でバックエンドは定義されたドキュメントを参照して実装を進めます。フロントエンドとバックエンドはOpenAPIで書かれた同じ仕様を頭にイメージし、共有しながら開発することができるのです。

Prismの起動はとっても簡単です。

$ prism mock openapi.yaml

これで、 openapi.yaml で定義されている通りのモックサーバーが立ち上がります。

モックサーバーはいつだって同じ答えを返す

APIモックサーバーは静的な存在です。プログラミング言語で動的に実装された実際のAPIとは異なり、同じエンドポイントをリクエストすれば必ず同じデータが返されます。だからこそテストを書くことができます。

しかし、モックサーバーのレスポンスにバリエーションが欲しい場面もあります。

  • データが0件だった場合の表示を確認したい
  • APIからエラーが返された場合のメッセージを組み込みたい
  • データの種類によるコンポーネントの出し分けをテストしたい

などなど、様々なケースがあると思います。そのような時に役立つのが、Preferヘッダーです。

Preferヘッダーはサーバーに要望を伝える

Preferヘッダーはクライアントからのリクエスト時に付与し、サーバー側にどのようにリクエストを処理してほしいかという要望を伝えるためのものです。

「Prefer」という言葉が表しているように、伝えるのは「~してもらいたい」という要望であって、強制力は持ちません。それに応えるかどうかはサーバーの実装次第ということです。例えば return=minimal という Prefer ヘッダーを渡すことで、「最小限のデータだけを返却してほしい」とサーバーに「お願い」することができます。

そして Prism には Prefer ヘッダーによってレスポンスをコントロールする機能が備わっていて、それが大変便利なのです。

PreferヘッダーでPrismモックサーバーの返答をコントロールする

ステータスコードでコントロールする

まず1つ目の方法はステータスコードによる制御です。例えばyamlで次のようなエンドポイントが定義されているとします。

paths:
  /hello:
    get:
      operationId: hello 
      responses:
        '200':
          description: OK
          content:
            ...
        '422':
          description: Unprocessable Entity
          content:
            ...

ステータスコード 200422 が定義されています。このモックサーバーに対してステータスコードを指定してリクエストを送ります。

// 422のレスポンスを取得する
const res = await fetch('/api/hello', {
  headers: {
    Prefer: 'code=422'
  }
});

この様に Prefer ヘッダーで code=xxx を渡すことでステータスコードによる出し分けをすることができ、状態を再現することができます。

ちなみにOpenAPIに定義していないステータスコードを付与した場合(例えば code=500 )は、指定したステータスコードに関わらず 404 が返却されます。単純にAPIからエラーが返された場合を検証したい時は、適当なステータスコードを指定してあげれば簡単です。

exampleでコントロールする

exampleによる指定では、より複雑な制御が可能になります。OpenAPI では examples で複数のレスポンスデータを定義することができますが、Preferヘッダーでどの example で返して欲しいかを指定することができます。

paths:
  /list:
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                ...
              examples:
                default:
                  value:
                    - id: 1
                      name: Item 1
                    - id: 2
                      name: Item 2
                empty:
                  value: []

上の例では、 default empty の2つの example が定義されています。ステータスコード同様に、 example={exampleName} の形で Prefer ヘッダーを積んであげれば希望するレスポンスデータが返される仕組みです。

// empty のレスポンスを取得する
const res = await fetch('/api/list', {
  headers: {
    Prefer: 'example=empty'
  }
});

Prefer ヘッダーで example が指定されていない場合は一番最初に定義されている example で返される仕様になっているため、この場合 example=default の指定は不要です。

e2eテストでPreferヘッダーを活用する

Preferヘッダーによるコントロールは、e2eテストと非常に相性が良いです。APIモックサーバに期待するレスポンスを出し分けてもらい、それぞれに対する挙動を検証することで複数パターンでのテストが行えます。

cypress の例

cypress では intercept でAPIへのリクエストやレスポンスを加工する事ができます。

it('空のデータが返されるケース', () => {
  cy.intercept('GET', '/api/list', (req) => {
    req.headers.Prefer = 'example=empty';
  });
  cy.visit('http://localhost:4000/list');
  cy.get('[data-cy=list]').should('contain.text', 'No items');
});

上の例では、リクエストに Prefer ヘッダーを付与して example=empty を指定しています。 http://localhost:4000/list のページでは /api/list APIへリクエストを送る実装になっていて、ここでは example=empty の指定により空のレスポンスが返却されます。

これで「空のデータが返ってきた場合の表示の検証」ができます。

playwright の例

playwright では route で同様の処理が行えます。

test('空のデータが返されるケース', async ({ page }) => {
  page.route('**/api/list', async (route) => {
    const res = await route.fetch({
      headers: {
        Prefer: 'example=empty'
      }
    })
    const json = await res.json();
    route.fulfill({ json });
  });


  await page.goto('http://localhost:4000/list');
  await expect(page.getByTestId('list')).toContainText('No items');
});

Preferヘッダーは安全に利用できる

Preferヘッダーの活用には安全面でのメリットがひとつあります。それは本番環境では機能しないということです。

例えばこのようなコードを誤ってコミットしてしまったとします。前提として人間はミスをするものなので、こういうことは起こり得ます。

const res = await fetch('/api/list', {
  headers: {
    Prefer: 'code=422' // エラーレスポンスのテスト用
  }
});

ステータスコード 422 のレスポンスを要求していますが、本番のAPIにとってはこれは全く意味を持たないコードです。ヘッダーによってレスポンスのステータスコードが変わるなどという実装は通常考えられません。 example についても同様で、サーバー側があえてそのように実装しなければ本番環境の挙動には影響せず、最悪の事態には到達しません。

もちろん、このようなデッドコードはプロダクションに混入させるべきではないので、十分に注意をして検証する必要があることは言うまでもないことです。

おわりに

プロジェクトチームにはデザイン・フロントエンド・バックエンドなど様々な職能が存在します。それぞれのフィールドで活躍するメンバーは、他のフィールドのメンバーと密にコミュニケーションをとり、プロダクトを作り上げていきます。そのための強力なツールのひとつとしてOpenAPI はフロントエンドとバックエンドの橋渡しをしてくれます。

Gaji-Labo では職能を横断して人が活躍するプロダクトチームを目指し、そのために有用な手法やツールなどを常に模索し、工夫しています。そうして一緒に成長していけるメンバーを、いつでも募集しています。興味を持たれた方は、是非ご一報いただけますと嬉しいです。

Gaji-Labo は新規事業やサービス開発に取り組む、事業会社・スタートアップへの支援を行っています。

弊社では、Next.js を用いた Web アプリケーションのフロントエンド開発をリードするフロントエンドエンジニアを募集しています!さまざまなプロダクトやチームに関わりながら、一緒に成長を体験しませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください!

求人応募してみる!


投稿者 Oikawa Hisashi

フロントエンドエンジニア。モダンなJavaScript開発に関心があります。 デザインからバックエンドまで網羅的にこなすマルチデザイナーとして長く活動してきた経験を活かして、これから関わる様々なものをデザインしていきたいです。チームもコミュニケーションもデザインするもの。ライフワークはピアノと水泳。