Working Together (but Separately) with MirageJS

We're using MirageJS to enable front-end and back-end teams to develop features asynchronously, without obstacles.

Hilary Beckprofile image

By Hilary Beck on 6/17/2021

Note: This post was written by Hilary Beck, with much-appreciated help from front-end engineer Christine Yi.


Front-end development and APIs — you’ll rarely see one without the other, but they don’t always get up and running at once. Maybe you’re developing a completely new feature that relies on a nonexistent endpoint, or perhaps the endpoint is just out of date, missing key chunks of data. Front-end and back-end are being built simultaneously, but one isn’t available to the other just yet.

Often, front-end developers will hard-code values or write their own mock data sets as a quick fix. But that doesn’t give us a full view of the finished product: edge cases, changes to state, latency and request time. A lot of things can be missed! Even something as simple as coding for a string when the response is a number can derail all the work when the API is ready.

Our front-end guild recently decided to tackle this problem with MirageJS, a dynamic, relatively lightweight library that functions as a fake server. It runs in the client and handles network requests just as a real server would; this resolves the friction of having to wait for API work to be completed before you can work on a feature. It also enables rapid prototyping and quicker asynchronous development. 

MirageJS factories favor framework-agnostic server-side models — structured, scaffolded-out data. When working on changes, back-end and front-end can get together at the discovery stage and decide what schema and API contracts will be. This easily translates to mocking out MirageJS’s data layer where the model definitions are made. Real interactions can be built by intercepting our local app’s network requests without a back-end API being present. There's no anticipating or hard-coding of expected data, and no need to modify UI code.

For instance, we recently redesigned our service catalog experience. We’ve already got some of the information we need in the existing endpoint, but it’s not quite as extensive as we'd like. In a review of design docs, we see new fields on our list page for values that we’re currently only getting as IDs. Both teams know what changes need to be made to the API, what the response should look like, and how it will be presented in the UI — and this is where MirageJS comes in.

In a new file MirageServer, we first import createServer and export a function makeServer, which accepts an options arg with an environment key (Mirage has two: default development and test), that creates a new server:

                                
import { createServer, Model } from "miragejs";

export function makeServer(environment) {
  return createServer({
	environment,

	models: {
	  service: Model,
	  organization: Model
	},

	routes() {
	  this.namespace = "api"
	  const api = window.firehydrantBaseAPIPath;
	  const path = 'services';

	  this.get(`${api}/${path}`, (schema, request) => {
		const serviceModels = schema.services.all().models;
		return {
		  "data": serviceModels,
		  "pagination": {
			  "count": serviceModels.length,
			  "page": 1,
			  "items": serviceModels.length,
			  "pages": 1,
			  "last": 1,
			  "prev": null,
			  "next": null
		  }
		}
	  });
	},

	seeds(server) {
	  server.create("service", serviceSample1)
	  server.create("service", serviceSample2)
	  server.create("service", serviceSample3)
	  server.create('organization', {})
	},
  })
}

const serviceSample1 = {
  id: "c22c7077-5f9c-420d-ac67-b542ba5b6f3a",
  name: "Chocolate",
  description: "Try-hard vice cleanse park etsy distillery wolf occupy quinoa.",
  slug: "chocolate",
  created_at: "2021-03-16T16:50:55.300Z",
  updated_at: "2021-03-16T16:50:55.300Z",
  labels: {},
  functionalities: [
	  {
		  id: "23112c1d-9759-41bd-9aa0-4da7fec0f441",
		  name: "my new funky functionality",
		  summary: "my new funky functionality",
		  description: "description of my funky functions",
		  created_at: "2021-03-25T18:42:34.238Z",
		  updated_at: "2021-03-25T18:42:34.238Z",
		  services: [
			  {
				  id: "c22c7077-5f9c-420d-ac67-b542ba5b6f3a",
				  name: "Chocolate",
				  description: "Try-hard vice cleanse park etsy distillery wolf occupy quinoa.",
				  slug: "blacktop-cup",
				  created_at: "2021-03-16T16:50:55.300Z",
				  updated_at: "2021-03-16T16:50:55.300Z",
				  labels: {}
			  }
		  ]
	  }
  ]
}

const serviceSample2 = {
  id: "3c42b771-b144-4f0f-a989-sdf12412423",
  name: "Strawberry",
  description: "We got some strawberries.",
  labels: {"tier": "1", "lifecycle": "production"},
  created_by_type: "User",
  created_by_id: "0ec6a6ff-d3ab-43e6-93ac-33afa60693a0",
  organization_id: "198526da-a53b-48d7-8796-8e154eb4b161",
  discarded_at: null,
  created_at: "Wed, 24 Mar 2021 18:34:43 UTC +00:00",
  updated_at: "Fri, 09 Apr 2021 16:40:35 UTC +00:00",
  slug: "strawberry",
  account_id: 1,
  tsv:
   "'bicycle':9 'chocolate':1 'etsy':6 'flexitarian':4 'kitsch':7 'level':2 'mixtape':8 'normcore':5 'rights':10 'ugh':3 'williamsburg':11",
  functionalities: []
 }

 const serviceSample3 = {
  id: "3c42b771-b144-4f0f-a989-s213asdfff33",
  name: "Vanilla",
  description: "We got some vanillar beans.",
  labels: {"tier": "1", "lifecycle": "production"},
  created_by_type: "User",
  created_by_id: "0ec6a6ff-d3ab-43e6-93ac-33afa60693a0",
  organization_id: "198526da-a53b-48d7-8796-8e154eb4b161",
  discarded_at: null,
  created_at: "Wed, 24 Mar 2021 18:34:43 UTC +00:00",
  updated_at: "Fri, 09 Apr 2021 16:40:35 UTC +00:00",
  slug: "vanilla",
  account_id: 1,
  tsv:
   "'bicycle':9 'chocolate':1 'etsy':6 'flexitarian':4 'kitsch':7 'level':2 'mixtape':8 'normcore':5 'rights':10 'ugh':3 'williamsburg':11",
  functionalities: []
 }
                                
                        

The seeds hook seeds our app with initial data so that we don’t start out empty. Within the routes() hook, we can define our route handler and use this.get to mock out our GET request.

We want to make sure Mirage is running when we render the services route, but not in production. Here’s where we call makeServer :

                                
if (process.env.NODE_ENV === "development") {
  makeServer({ environment: "development" })
}
                                
                        

When the new API is ready, it won’t require much to transition off Mirage. Remember, our requests are already pointing at the real API server; Mirage only intercepts them. Nothing has changed about our data fetch — we just need to delete the route from our mock server, remove the makeServer call, and we’re good to ship.

Mirage’s test environment runs with zero delays, suppresses all logs, and ignores the seeds() hook to prevent data leak. This makes it easy to test requests and responses in general without having to rely on configuring your local database. 

With any project, issues of scaling and maintenance arise. As an organization grows and undergoes constant changes to the API, your MirageJS will need manual updates. You might have begun playing around with a one-off fake server on the component level, but it makes more sense to have a larger, centralized server like you would in production. For time-sensitive projects, the additional work is something to consider, but MirageJS does offer plenty of solutions for more complex configurations (mocking GUIDs, cookie responses) and ultimately behaves like any API contract would.

It’s given peace of mind to everyone, from front-end to back-end to design, while radically transforming our app. We can’t wait to see how it will help us collaborate even more.

See FireHydrant in action

See how service catalog, incident management, and incident communications come together in a live demo.

Get a demo