Route handlers

Route handlers let you define which URLs your Mirage server can handle.

The simplest route handler maps a URL to an object:

this.get("/movies", { movies: ["Interstellar", "Inception", "Dunkirk"] })

Now when your app makes a GET request to /movies, it will receive this object as JSON data.

If your API is on a different host or port than your app, set urlPrefix:

    routes() {
      this.urlPrefix = 'http://localhost:3000';

You can also write function route handlers by passing in a function as the second argument:

this.get("/movies", (schema, request) => {
  return ["Interstellar", "Inception", "Dunkirk"]
})

Function route handlers are the most flexible way to write route handlers since they give you access to Mirage's data layer and the request object. Most of your route handlers will be functions.

You can use any of the HTTP verbs to define your API routes. Each verb method has the same signature. The first argument is the path (URL) and the second is a function that returns the response.

this.get('/movies', () => { ... });
this.post('/movies', () => { ... });
this.patch('/movies/:id', () => { ... });
this.put('/movies/:id', () => { ... });
this.del('/movies/:id', () => { ... });
this.options('/movies', () => { ... });

Timing

The last argument to a route handler is an options object you can use to adjust the timing. Use this to delay the response of a particular route and see how your app behaves when communicating with a slow network.

this.get(
  "/movies",
  () => {
    return ["Interstellar", "Inception", "Dunkirk"]
  },
  { timing: 4000 }
)

The default delay is 400ms during development, and 0 during testing (so your tests run fast).

You can also set a global timing parameter for all routes. Individual timing parameters override the global setting.

createServer({
  routes() {
    this.namespace = "api"
    this.timing = 2000

    this.get("/movies", () => {
      return ["Interstellar", "Inception", "Dunkirk"]
    })

    this.get(
      "/complex-query",
      () => {
        return [1, 2, 3, 4, 5]
      },
      { timing: 3000 }
    )
  },
})

If you want to add delays to a test, you can override the timing for individual tests by putting the timing parameter in your test

test("this route works with a delay", function () {
  server.timing = 10000

  // ...
})

Because the server is reset after each test, this option won't leak into the rest of your suite.

Accessing the data layer

Route handlers receive schema as their first parameter, which lets them access Mirage's data layer:

this.get("/movies", (schema) => {
  return schema.movies.all()
})

Most of your route handlers will interact with the data layer in some way.

The second parameter is the request object, which contains information about the request your app made. For example, you can access dynamic URL segments from it:

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

  return schema.movies.find(id)
})

You can also access the request body, for example to handle a POST or PATCH request that contains data sent over by the app:

this.post("/movies", (schema, request) => {
  let attrs = JSON.parse(request.requestBody)

  return schema.movies.create({ attrs })
})

The normalizedRequestAttrs helper (documented below) provides some sugar for working with the request data.

Dynamic paths and query params

The request object that's injected into your route handlers contains any dynamic route segments and query params.

To define a route that has a dynamic segment, use colon syntax (:segment) in your path. The dynamic piece will be available via request.params.[segment]:

this.get("/authors/:id", (schema, request) => {
  let id = request.params.id

  return schema.authors.find(id)
})

Query params from the request can also be accessed via request.queryParams.[param].

Status codes and headers

By default, Mirage sets the HTTP status code of a response based on the verb being used for the route:

  • GET is 200
  • PATCH/PUT is 204
  • POST is 201
  • DEL is 204

PATCH/PUT and POST change to 200 if there is a response body.

Additionally, a header for Content-Type is set to application/json.

You can customize both the response code and headers by returning an instance of the Response class in your route handler:

import { createServer, Model, Response } from "miragejs"

createServer({
  models: {
    author: Model,
  },
  routes() {
    this.post("/authors", function (schema, request) {
      let attrs = JSON.parse(request.requestBody).author

      if (attrs.name) {
        return schema.authors.create(attrs)
      } else {
        return new Response(
          400,
          { some: "header" },
          { errors: ["name cannot be blank"] }
        )
      }
    })
  },
})

External origins

You can use Mirage to simulate other-origin requests. By default, a route like

this.get('/contacts', ...)

will intercept request made to the same origin that's serving your app. To handle a different origin, use a fully qualified domain name:

this.get('http://api.twitter.com/v1', ...)

If your entire app uses an external (other-origin) API, you can globally configure the domain via urlPrefix:

createServer({
  routes() {
    this.urlPrefix = 'https://my.api.com';

    // This route will intercept requests to https://my.api.com/contacts
    this.get('/contacts', ...)
  }
})

Helpers

There are several helpers available when writing function route handlers.

If you're new to Mirage, don't worry about understanding these yet. They are helpful when writing more advanced route handlers.

serialize

This helper returns the JSON for the given Model or Collection after passing it through the Serializer layer. It's useful if you want to do some final munging on the serialized JSON before returning it.

// Note: Be sure to use function() here, rather than () => {}
this.get("/movies", function (schema) {
  let movies = schema.movies.all()
  let json = this.serialize(movies)

  json.meta = { page: 1 }

  return json
})

By default this method uses the named serializer for the given Model or Collection. You can pass in a specific serializer name as the second argument:

this.get("/movies", function (schema) {
  let movies = schema.movies.all()
  let json = this.serialize(movies, "sparse-movie")

  json.meta = { page: 1 }

  return json
})

You'll learn more about Serializers later in these guides.

normalizedRequestAttrs

This helper returns the body of a request in a normalized form, suitable for working with and creating records. It essentially removes the formatting logic from your API payload, giving you the underlying attributes which you can then use to modify Mirage's database.

For example, if your app makes a POST request with this data

// POST /users

{
  "data": {
    "type": "users",
    "attributes": {
      "first-name": "Conan",
      "middle-name": "the",
      "last-name": "Barbarian"
    },
    "relationships": {
      "team": {
        "data": {
          "type": "teams",
          "id": 1
        }
      }
    }
  }
}

then normalizedRequestAttrs() could be used like this

this.post("/users", function (schema, request) {
  let attrs = this.normalizedRequestAttrs()
  /*
    attrs is this object:
      {
        firstName: 'Conan',
        middleName: 'the',
        lastName: 'Barbarian',
        teamId: '1'
      }
  */
  return schema.users.create(attrs)
})

Note that attribute keys were camelCased, and the team foreign key was extracted. This is because a user owns the team foreign key; if another relationship were included in the request but the user did not own its foreign key, it would not have been extracted.

This helper method leverages your serializer's normalize method. In the example above, it's assumed that the app was using the JSONAPISerializer, which comes with the #normalize method already written. If you're not using one of the bundled serializers, you'll need to implement #normalize and have it return a JSON:API document to take advantage of this method.

Additionally, you'll need to use a full function here, as opposed to an ES6 arrow function (e.g () => { ... }). This is because normalizedRequestAttrs requires the this context from the function handler, and an arrow function would bind this from the outer scope.

normalizedRequestAttrs() relies on a modelName to work and attempts to automatically detect it based on the URL of the request. If you use conventional URLs – for example, PATCH /users/1 – the helper should work. If you are using something custom – for example, PATCH /users/edit/1 – you’ll need to provide the modelName to the helper as the first argument:

this.patch("/users/edit/:id", function (schema, request) {
  let attrs = this.normalizedRequestAttrs("user")
  // ...
})

That's it on writing low-level function route handlers!

Function route handlers are flexible, but also cumbersome to write out for every endpoint. If you're working with an API that's conventional enough, hopefully you'll be writing fewer function route handlers and more Shorthands. Let's discuss those next!