シリアライザー

シリアライザーとは、ルートハンドラーから返されたモデルまたはコレクションを、フロントエンドアプリが期待する形式のJSONペイロードに変換する役割を担うオブジェクトです。

たとえば、このルートハンドラーはMovieモデルを返します。

this.get("movies/:id", (schema, request) => {
  return schema.movies.find(request.params.id)
})

シリアライザーは、そのJavaScriptオブジェクトをこのJSONペイロードに変換します。

// GET /movies/1

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    }
  }
}

シリアライザーは、モデルのリレーションシップグラフを辿ることが多い、適切にフォーマットされたJSONレスポンスを生成するため、データ層とやり取りするMirageのアーキテクチャの最後の主要部分です。

どのように動作するか見てみましょう。

使用するシリアライザーの選択

Mirageのシリアライザーを使用する最初のステップは、どの組み込みシリアライザーから開始するかを選択することです。これは、バックエンドがJavaScriptアプリにデータを供給するために使用するJSON形式によって異なります。

上記のJSONペイロードは、JSON:API仕様に従うAPIの例です。属性とリレーションシップを区別し、名前付きおよびポリモーフィックなリレーションシップ、リンク、クエリパラメーターインクルードなどをサポートする非常に特定の構造を持っています。また、厳密に定義されていない他の形式に存在する多くの問題を解決します。

既存のバックエンドAPIがJSON:APIを使用している場合、Mirageには、複雑な処理を代行するJSONAPISerializerが付属しています。また、新しいアプリを始める場合は、JSON:APIの使用を検討してください。APIの構築中に発生する多くの問題を解決し、チームが不必要な議論を避けるのに役立つためです。

もちろん、APIシリアライズ形式としてJSON:APIを使用していないJavaScriptアプリもたくさんあります。

Mirageには、一般的なバックエンドの形式に合わせた、名前付きのシリアライザーが2つ付属しています。ActiveModelSerializerは、active_model_serializer gemで構築されたRails APIに似たAPIを模倣することを目的としており、RestSerializerは、他の多くの一般的なAPIの出発点として適しています。

独自のバックエンドAPIの形式に応じて、最も近いシリアライザーを出発点として選択し、本番環境の形式に合わせてカスタマイズする必要があります。これについては、このガイドの後半で詳しく説明します。

シリアライザーの定義

適切なシリアライザーを選択したら、次のようにアプリケーション全体のデフォルトのシリアライザーとして定義します。

import { createServer, RestSerializer } from "miragejs"

createServer({
  serializers: {
    application: RestSerializer,
  },
})

アプリケーションシリアライザーは、システム内のすべてのモデルとコレクションに使用されるデフォルトのシリアライザーです。

特定のモデルタイプに対してシリアライザーをカスタマイズする必要がある場合は、アプリケーションシリアライザーよりも優先されるモデル固有のシリアライザーを定義できます。

import { createServer, RestSerializer } from "miragejs"

createServer({
  serializers: {
    application: RestSerializer,
    movie: RestSerializer.extend({
      include: ["castMembers"],
    }),
  },
})

このガイドの後半で、モデル固有のカスタマイズの例を紹介します。

通常、モデル固有のカスタマイズに加えて、アプリケーション全体に適用したいカスタマイズがある場合があります。このため、ベストプラクティスは、アプリケーションシリアライザーを拡張するモデル固有のシリアライザーを使用することです。これは、次のようにして実現できます。

import { createServer, RestSerializer } from "miragejs"

let ApplicationSerializer = RestSerializer.extend({
  root: false,
})

createServer({
  serializers: {
    application: ApplicationSerializer,
    movie: ApplicationSerializer.extend({
      include: ["castMembers"],
    }),
  },
})

これで、モデル固有のシリアライザーを作成する際に、アプリケーション全体のカスタマイズが維持されます。

シリアライザーのカスタマイズ

アプリケーションのシリアライザーをカスタマイズする際は、ほとんどの場合、Mirageのデフォルトを微調整することになります。

たとえば、アプリが属性名をPascalCaseで期待する場合

// GET /movies/1

{
  Id: '1',
  ReleaseDate: 'Interstellar'
}

シリアライザーのkeyForAttributeメソッドをオーバーライドできます。

import { RestSerializer } from "miragejs"
import { camelCase, upperFirst } from "lodash"

let ApplicationSerializer = RestSerializer.extend({
  keyForAttribute(attr) {
    return upperFirst(camelCase(attr))
  },
})

利用可能なすべてのカスタマイズフックの詳細については、各シリアライザーのAPIドキュメントを参照してください。

リレーションシップ

リレーションシップは、バックエンドがリレーションシップを処理する方法が多数あるため、シリアライザーのもう1つの重要な側面です。

たとえば、デフォルトでは、JSONAPISerializerは、クエリパラメーターのincludeを介して要求された場合にのみ、リレーションシップ情報を含めます。

/* GET /movies/1?include=cast-members */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    },
    "relationships": {
      "cast-members": {
        "data": [
          { "type": "people", "id": "1" },
          { "type": "people", "id": "2" },
          { "type": "people", "id": "3" }
        ]
      }
    }
  },
  "included": [
    { "id": "1", "type": "people", "attributes": { "name": "Susan" } },
    { "id": "2", "type": "people", "attributes": { "name": "Bob" } },
    { "id": "3", "type": "people", "attributes": { "name": "Jane" } }
  ]
}

それ以外の場合は、プライマリリソースのみが含まれます。

/* GET /movies/1 */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    }
  }
}

ただし、一部のAPIでは、リクエストでクエリパラメーターのincludeが使用されているかどうかに関係なく、リソースのリレーションシップIDがすべて含まれます。

JSONAPISerializerには、これを有効にするオプションがあります。

JSONAPISerializer.extend({
  alwaysIncludeLinkageData: true,
})

これで、/movies/1へのGETリクエストは、このペイロードで応答します。

/* GET /movies/1 */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    },
    "relationships": {
      "cast-members": {
        "data": [
          { "type": "people", "id": "1" },
          { "type": "people", "id": "2" },
          { "type": "people", "id": "3" }
        ]
      }
    }
  }
}

これで、JavaScriptアプリはこれらのIDを使用して、関連するキャストメンバーを後でフェッチできます。

他のAPIコントラクトは、関連データをフェッチするためのリンクで応答します。JSONAPISerializerにもこれに対応するフックがあります。

JSONAPISerializer.extend({
  links(movie) {
    return {
      "cast-members": {
        related: `/api/movies/${movie.id}/cast-members`,
      },
    }
  },
})

これで、/movies/1へのGETリクエストは、このペイロードで応答します。

/* GET /movies/1 */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    },
    "relationships": {
      "cast-members": {
        "links": {
          "related": "/api/movies/1/cast-members"
        }
      }
    }
  }
}

他のシリアライザーにも、関連データをロードする方法を制御するメカニズムがあります。詳細については、必ずAPIドキュメントを確認してください。

シリアライズされたJSONの操作

ほとんどのルートハンドラーはモデルまたはコレクションインスタンスを返し、シリアライズロジックはシリアライザーに任せる必要がありますが、ルートハンドラーで直接最終的なシリアライズロジックを実行すると便利な場合があります。

ルートハンドラーでthis.serializeヘルパーメソッドを使用してこれを行うことができます。正しいthisにアクセスできるように、ファットアローの代わりにfunctionを使用してください。

createServer({
  routes() {
    this.get("/movies", function (schema, request) {
      let movies = schema.movies.all()
      let json = this.serialize(movies)

      json.meta.size = movies.length

      return json
    })
  },
})

serializeヘルパーは、通常のルックアップロジックを使用して、最初にモデル固有のシリアライザーを確認し、次にデフォルトのアプリケーションシリアライザーにフォールバックします。

特別なケースがある場合は、シリアライザーの名前を2番目の引数として渡すことで、特定のシリアライザーを使用することもできます。

import { createServer, RestSerializer } from "miragejs"

let ApplicationSerializer = RestSerializer.extend()

createServer({
  serializers: {
    application: ApplicationSerializer,
    movieWithRelationships: ApplicationSerializer.extend({
      include: ["castMembers", "reviews"],
    }),
  },

  routes() {
    this.get("/movies/:id", function (schema, request) {
      let movie = schema.movies.find(request.params.id)
      let json = this.serialize(movie, "movie-with-relationships")

      return json
    })
  },
})

文字列名は、シリアライザーを定義するために使用したキー(movieWithRelationships)のケバブケースバージョン(movie-with-relationships)であることに注意してください。


一般に、提供されているフックを使用して、ほとんどのモデルで機能するApplicationSerializerを実装するように努める必要があります。

APIに関しては、一貫性が重要です。本番環境のAPIが一貫しているほど、Mirageでの実装が容易になります。Mirageサーバーを使用して、フロントエンドチームとバックエンドチーム間でAPIコントラクトの矛盾についてやり取りしてください。従来のAPIコントラクトを使用すると、Mirageだけでなく、他のJavaScriptアプリでもコードを削減できます。

シリアライザーレイヤーをカスタマイズするために利用できるすべてのフックについては、必ずAPIドキュメントを確認してください。


これで、Mirageの主要な概念をすべて説明しました。Mirageを使用してJavaScriptアプリケーションを効果的にテストする方法を見ていきましょう。