Don’t Wait! Mock the API

Avatar of Marko Ilic
Marko Ilic on

Today we have a loose coupling between the front end and the back end of web applications. They are usually developed by separate teams, and keeping those teams and the technology in sync is not easy. To solve part of this problem, we can “fake” the API server that the back end tech would normally create and develop as if the API or endpoints already exist.

The most common term used for creating simulated or “faking” a component is mocking. Mocking allows you to simulate the API without (ideally) changing the front end. There are many ways to achieve mocking, and this is what makes it so scary for most people, at least in my opinion. 

Let’s cover what a good API mocking should look like and how to implement a mocked API into a new or existing application.

Note, the implementation that I am about to show is framework agnostic — so it can be used with any framework or vanilla JavaScript application.

Mirage: The mocking framework

The mocking approach we are going to use is called Mirage, which is somewhat new. I have tested many mocking frameworks and just recently discovered this one, and it’s been a game changer for me.

Mirage is marketed as a front-end-friendly framework that comes with a modern interface. It works in your browser, client-side, by intercepting XMLHttpRequest and Fetch requests.

We will go through creating a simple application with mocked API and cover some common problems along the way.

Mirage setup

Let’s create one of those standard to-do applications to demonstrate mocking. I will be using Vue as my framework of choice but of course, you can use something else since we’re working with a framework-agnostic approach.

So, go ahead and install Mirage in your project:

# Using npm
npm i miragejs -D


# Using Yarn
yarn add miragejs -D

To start using Mirage, we need to setup a “server” (in quotes, because it’s a fake server). Before we jump into the setup, I will cover the folder structure I found works best.

/
├── public
├── src
│   ├── api
│   │   └── mock
│   │       ├── fixtures
│   │       │   └── get-tasks.js
│   │       └── index.js
│   └── main.js
├── package.json
└── package-lock.json

In a mock directory, open up a new index.js file and define your mock server:

// api/mock/index.js
import { Server } from 'miragejs';


export default function ({ environment = 'development' } = {}) {
  return new Server({
    environment,


    routes() {
      // We will add our routes here
    },
  });
}

The environment argument we’re adding to the function signature is just a convention. We can pass in a different environment as needed.

Now, open your app bootstrap file. In our case, this is he src/main.js file since we are working with Vue. Import your createServer function, and call it in the development environment.

// main.js
import createServer from './mock'


if (process.env.NODE_ENV === 'development') {
    createServer();
}

We’re using the process.env.NODE_ENV environment variable here, which is a common global variable. The conditional allows Mirage to be tree-shaken in production, therefore, it won’t affect your production bundle.

That is all we need to set up Mirage! It’s this sort of ease that makes the DX of Mirage so nice.

Our createServer function is defaulting it to development environment for the sake of making this article simple. In most cases, this will default to test since, in most apps, you’ll call createServer once in development mode but many times in test files.

How it works

Before we make our first request, let’s quickly cover how Mirage works.

Mirage is a client-side mocking framework, meaning all the mocking will happen in the browser, which Mirage does using the Pretender library. Pretender will temporarily replace native XMLHttpRequest and Fetch configurations, intercept all requests, and direct them to a little pretend service that the Mirage hooks onto.

If you crack open DevTools and head into the Network tab, you won’t see any Mirage requests. That’s because the request is intercepted and handled by Mirage (via Pretender in the back end). Mirage logs all requests, which we’ll get to in just a bit.

Let’s make requests!

Let’s create a request to an /api/tasks endpoint that will return a list of tasks that we are going to show in our to-do app. Note that I’m using axios to fetch the data. That’s just my personal preference. Again, Mirage works with native XMLHttpRequest, Fetch, and any other library.

// components/tasks.vue
export default {
  async created() {
    try {
      const { data } = await axios.get('/api/tasks'); // Fetch the data
      this.tasks = data.tasks;
    } catch(e) {
      console.error(e);
    }
  }
};

Opening your JavaScript console — there should be an error from Mirage in there:

Mirage: Your app tried to GET '/api/tasks', but there was no route defined to handle this request.

This means Mirage is running, but the router hasn’t been mocked out yet. Let’s solve this by adding that route.

Mocking requests

Inside our mock/index.js file, there is a routes() hook. Route handlers allow us to define which URLs should be handled by the Mirage server.

To define a router handler, we need to add it inside the routes() function.

// mock/index.js
export default function ({ environment = 'development' } = {}) {
    // ...
    routes() {
      this.get('/api/tasks', () => ({
        tasks: [
          { id: 1, text: "Feed the cat" },
          { id: 2, text: "Wash the dishes" },
          //...
        ],
      }))
    },
  });
}

The routes() hook is the way we define our route handlers. Using a this.get() method lets us mock GET requests. The first argument of all request functions is the URL we are handling, and the second argument is a function that responds with some data.

As a note, Mirage accepts any HTTP request type, and each type has the same signature:

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

We will discuss the schema and request parameters of the callback function in a moment.

With this, we have successfully mocked our route and we should see inside our console a successful response from Mirage.

Screenshot of a Mirage response in the console showing data for two task objects with IDs 1 and 2.

Working with dynamic data

Trying to add a new to-do in our app won’t be possible because our data in the GET response has hardcoded values. Mirage’s solution to this is that they provide a lightweight data layer that acts as a database. Let’s fix what we have so far.

Like the routes() hook, Mirage defines a seeds() hook. It allows us to create initial data for the server. I’m going to move the GET data to the seeds() hook where I will push it to the Mirage database.

seeds(server) {
  server.db.loadData({
    tasks: [
      { id: 1, text: "Feed the cat" },
      { id: 2, text: "Wash the dishes" },
    ],
  })
},

I moved our static data from the GET method to seeds() hook, where that data is loaded into a faux database. Now, we need to refactor our GET method to return data from that database. This is actually pretty straightforward — the first argument of the callback function of any route() method is the schema.

this.get('/api/tasks', (schema) => {
  return schema.db.tasks;
})

Now we can add new to-do items to our app by making a POST request:

async addTask() {
  const { data } = await axios.post('/api/tasks', { data: this.newTask });
  this.tasks.push(data);
  this.newTask = {};
},

We mock this route in Mirage by creating a POST /api/tasks route handler:

this.post('/tasks', (schema, request) => {})

Using the second parameter of the callback function, we can see the sent request.

Screenshot of the mocking server request. The requestBody property is highlighted in yellow and contains text data that says Hello CSS-Tricks.

Inside the requestBody property is the data that we sent. That means it’s now available for us to create a new task.

this.post('/api/tasks', (schema, request) => {
  // Take the send data from axios.
  const task = JSON.parse(request.requestBody).data


  return schema.db.tasks.insert(task)
})

The id of the task will be set by the Mirage’s database by default. Thus, there is no need to keep track of ids and send them with your request — just like a real server.

Dynamic routes? Sure!

The last thing to cover is dynamic routes. They allow us to use a dynamic segment in our URL, which is useful for deleting or updating a single to-do item in our app.

Our delete request should go to /api/tasks/1, /api/tasks/2, and so on. Mirage provides a way for us  to define a dynamic segment in the URL, like this:

this.delete('/api/tasks/:id', (schema, request) => {
  // Return the ID from URL.
  const id = request.params.id;


  return schema.db.tasks.remove(id);
})

Using a colon (:) in the URL is how we define a dynamic segment in our URL. After the colon, we specify the name of the segment which, in our case, is called id and maps to the ID of a specific to-do item. We can access the value of the segment via the request.params object, where the property name corresponds to the segment name — request.params.id. Then we use the schema to remove an item with that same ID from the Mirage database.

If you’ve noticed, all of my routes so far are prefixed with api/. Writing this over and over can be cumbersome and you may want to make it easier. Mirage offers the namespace property that can help. Inside the routes hook, we can define the namespace property so we don’t have to write that out each time.

routes() {
 // Prefix for all routes.
 this.namespace = '/api';


 this.get('/tasks', () => { ... })
 this.delete('/tasks/:id', () => { ... })
 this.post('/tasks', () => { ... })
}

OK, let’s integrate this into an existing app

So far, everything we’ve looked at integrates Mirage into a new app. But what about adding Mirage to an existing application? Mirage has you covered so you don’t have to mock your entire API.

The first thing to note is that adding Mirage to an existing application will throw an error if the site makes a request that isn’t handled by Mirage. To avoid this, we can tell Mirage to pass through all unhandled requests.

routes() {
  this.get('/tasks', () => { ... })
  
  // Pass through all unhandled requests.
  this.passthrough()
}

Now we can develop on top of an existing API with Mirage handling only the missing parts of our API.

Mirage can even change the base URL of which it captures the requests. This is useful because, usually, a server won’t live on localhost:3000 but rather on a custom domain.

routes() {
 // Set the base route.
 this.urlPrefix = 'https://devenv.ourapp.example';


 this.get('/tasks', () => { ... })
}

Now, all of our requests will point to the real API server, but Mirage will intercept them like it did when we set it up with a new app. This means that the transition from Mirage to the real API is pretty darn seamless — delete the route from the mock server and things are good to go.

Wrapping up

Over the course of five years, I have used many mocking frameworks, yet I never truly liked any of the solutions out there. That was until recently, when my team was faced with a need for a mocking solution and I found out about Mirage.

Other solutions, like the commonly used JSON-Server, are external processes that need to run alongside the front end. Furthermore, they are often nothing more than an Express server with utility functions on top. The result is that front-end developers like us need to know about middleware, NodeJS, and how servers work… things many of us probably don’t want to handle. Other attempts, like Mockoon, have a complex interface while lacking much-needed features. There’s another group of frameworks that are only used for testing, like the popular SinonJS. Unfortunately, these frameworks can’t be used to mock the regular behavior.

My team managed to create a functioning server that enables us to write front-end code as if we were working with a real back-end. We did it by writing the front-end codebase without any external processes or servers that are needed to run. This is why I love Mirage. It is really simple to set up, yet powerful enough to handle anything that’s thrown at it. You can use it for basic applications that return a static array to full-blown back-end apps alike — regardless of whether it’s a new or existing app.

There’s a lot more to Mirage beyond the implementations we covered here. A working example of what we covered can be found on GitHub. (Fun fact: Mirage also works with GraphQL!) Mirage has well-written documentation that includes a bunch of step-by-step tutorials, so be sure to check it out.