Serializers

A serializer is an object responsible for transforming a Model or Collection that's returned from your route handlers into a JSON payload that's formatted the way your frontend app expects.

For example, this route handler returns a Movie model:

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

A Serializer turns that JavaScript object into this JSON payload:

// GET /movies/1

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

Serializers are the last main part of Mirage's architecture that interacts with the data layer, because producing a well-formatted JSON response often involves traversing the relationship graph of your models.

Let's see how they work.

Choosing which serializer to use

The first step in working with Mirage's serializers is to choose which included serializer to start with, which in turn depends on what JSON format your backend uses to serve data to your JavaScript app.

The JSON payload above is an example of an API that follows the JSON:API spec. It has a very specific structure that differentiates attributes from relationships, supports named and polymorphic relationships, links, query param includes and more. It also solves a lot of problems that exist in other formats that are less rigorously defined.

If your existing backend API does use JSON:API, Mirage ships with a JSONAPISerializer that will do the heavy lifting for you. And if you're starting a new app, consider using JSON:API, since it solves many problems you'll run into while building an API and can help your team avoid bike shedding.

Of course, there are plenty of JavaScript apps that don't use JSON:API as their API serialization format.

Mirage ships with two other named serializers that match popular backend formats. ActiveModelSerializer is intended to mimic APIs that resemble Rails APIs built with the active_model_serializer gem, and RestSerializer is a good starting point for many other common APIs.

Depending on your own backend API's format, you'll need to choose the closest serializer as a starting point and customize it to match your production format. We'll talk more about that later in this guide.

Defining serializers

Once you've selected the appropriate serializer to start with, define it as your default application-wide serializer like this:

import { Server, RestSerializer } from "miragejs"

new Server({
  serializers: {
    application: RestSerializer,
  },
})

The application serializer is the default one used for every Model and Collection in your system.

If you need to customize a serializer for a particular model type, you can define model-specific serializers that take precedence over your application serializer.

import { Server, RestSerializer } from "miragejs"

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

You'll see examples of the kinds of model-specific customizations you might want later in the guide.

Typically you might have application-wide customizations that you'd want to make, in addition to model-specific customizations. Because of this, the best practice is to use model-specific serializers that extend from your application serializer. You might achieve that like this:

import { Server, RestSerializer } from "miragejs"

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

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

Now the application-wide customizations will be maintained as you create model-specific serializers.

Customizing serializers

When it comes to customizing your application's serializers, you'll mostly be tweaking Mirage's defaults.

For example, if your app expects attribute names to be PascalCase

// GET /movies/1

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

you might override the Serializer's keyForAttribute method:

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

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

See the API docs for each serializer to learn more about all the customization hooks available.

Relationships

Relationships are another important aspect of Serializers, since backends have many different ways of dealing with relationships.

For example, out of the box the JSONAPISerializer will only include relationship information if its requested via query param includes:

/* 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" } }
  ]
}

Otherwise, only the primary resource is included:

/* GET /movies/1 */

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

But some APIs will include all of a resource's relationship IDs, regardless if query param includes are used in the request or not.

There's an option on JSONAPISerializer that enables this:

JSONAPISerializer.extend({
  alwaysIncludeLinkageData: true,
})

Now, a GET request to /movies/1 would respond with this payload:

/* 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" }
        ]
      }
    }
  }
}

And now, your JavaScript app could use these ids to subsequently fetch the related cast members.

Other API contracts work by responding with links to fetch related data. The JSONAPISerializer also has a hook for this:

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

Now a GET request to /movies/1 would respond with this payload:

/* GET /movies/1 */

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

The other serializers also have mechanisms controlling how related data can be loaded. Be sure to check out the API docs for all the details.

Working with serialized JSON

While most route handlers should return a Model or Collection instance and leave the serialization logic up to the Serializer, sometimes it can be convenient to perform some final serialization logic directly in your route handler.

You can use the this.serialize helper method in a route handler to do this - make sure to use a function instead of a fat arrow so you have access to the correct this:

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

      json.meta.size = movies.length

      return json
    })
  },
})

The serialize helper will use the typical lookup logic to first check for a model-specific serializer, and then fall back to the default Application serializer.

You can also use a specific serializer if you have a special case by passing in the name of the serializer as a second argument:

import { Server, RestSerializer } from "miragejs"

let ApplicationSerializer = RestSerializer.extend()

new Server({
  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
    })
  },
})

Note that the string name is the kebab-case version (movie-with-relationships) of key you used to define the serializer (movieWithRelationships).


In general, you should strive to use the provided hooks to implement an ApplicationSerializer that works for the majority of your models.

Consistency is the name of the game when it comes to APIs. The more consistent your production API is, the easier it will be to implement it in Mirage. Use your Mirage server to communicate between your frontend and backend teams about inconsistencies in your API contract. Conventional API contract contracts will help you write less code – not only in Mirage, but also in the rest of your JavaScript app!

Be sure to check out the API docs to learn about all the hooks available to customize your serializer layer.


Now that we've covered the ins and outs of Mirage's data layer, we're ready to see how we can use Mirage to effectively test our JavaScript application.