Mocking APIs Without the Backend

Front-end projects almost always depend on a web service for authentication or user data, which creates friction when the backend team hasn't delivered the API yet. You could proxy to a local backend instance (which most front-end developers don't have) or comment out real requests and replace them with mock data (which is messy to reverse before production). API mocking solves this by simulating the actual API from the front end, intercepting requests meant for a real backend.

This approach improves the development experience in two significant ways. First, building features no longer blocks on production APIs being ready. Second, a complete front end can be shared and run without requiring a backend service at all.

Why Mirage JS?

Mirage JS originated in the Ember community before its public release was announced by Sam Selikoff in January 2020. It's an API mocking library for Vue.js, React, Angular, and Ember that addresses the pain of testing without actual backend APIs.

Alternatives like Axios interceptors and Typicode's JSON server exist, but Mirage JS stands out because it stays out of the way of the development process while remaining lightweight and powerful. It comes with built-in capabilities to replicate real production scenarios, such as simulating slow networks through its timing option.

Now let's set it up with Vue.js and build a simple todos API mock.

Setting Up a Vue Project

Create a fresh Vue project using the Vue CLI. Run the following from the directory where you want the project to live:

vue create miragejs-demo-vue 

Then navigate into the project and run either yarn serve or npm run serve.

Installing Mirage JS

Add Mirage JS as a development dependency:

yarn add -D miragejs

Or if you use NPM:

npm install --save-dev miragejs

Putting API mockup into effect requires defining a server file and wiring it into the Vue app.

Creating a Mock Server

Create a server.js file in the /src directory. This file sets up a new Mirage JS fake server that intercepts any API calls matching routes you define. Start with this:

import { Server, Model } from 'miragejs'

export function makeServer({ environment = "development" } = {}) {

let server = new Server({
  environment,

    models: {
      todo: Model,
    },

  seeds(server) {
  server.create("todo", { content: "Learn Mirage JS" })
  server.create("todo", { content: "Integrate With Vue.js" })
  },

  routes() {

    this.namespace = "api"

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

  return server
}

This code imports the Server and Model classes from the miragejs package.

  • Server: The class that instantiates the Mirage JS server acting as your fake backend.
  • Model: The class that defines models that structure entries in Mirage JS's in-memory database.
import { Server, Model } from 'miragejs'

The snippet exports a function called makeServer from src/server.js. A parameter sets Mirage's environment mode to development, which we'll see used again later for testing.

Inside makeServer

The body of makeServer handles several configuration tasks when instantiating the Server class:

let server = new Server({})
{
  environment,

  models: {
    todo: Model,
  },

  seeds(server) {
  server.create("todo", { content: "Learn Mirage JS" })
  server.create("todo", { content: "Integrate With Vue.js" })
  },

  routes() {

    this.namespace = "api"

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

The environment option passes the parameter initialized in the function definition.

models: {
    todo: Model,
  },

The models option takes an object describing which models we want Mirage to mock. Our setup creates a todo model instantiated from the Model class.

seeds(server) {
server.create("todo", { content: "Learn Mirage JS" })
server.create("todo", { content: "Integrate With Vue.js" })
},

The seeds method accepts a server parameter and creates initial data — or seeds — for the model. The server's create method takes the model name as the first argument, followed by an object containing the properties of each seed.

server.create("todo", { content: "Learn Mirage JS" })
server.create("todo", { content: "Integrate With Vue.js" })
routes() {

    this.namespace = "api"

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

The routes method defines the mock API endpoints. The code below sets a namespace so the todo route becomes /api/todos.

this.namespace = "api"
this.get("/todos", schema => {
  return schema.todos.all()
})

A get route is created with this.get(), which expects the route path and a handler function that takes schema as an argument. The schema object is how you interact with Mirage's ORM and its in-memory database:

return schema.todos.all()

Connecting to Vue

Vue doesn't know about the server yet, so import it into main.js:

import { makeServer } from "./server"

Then call the makeServer function conditionally, so Mirage only runs in development:

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

The Mocked API in Action

The setup is complete. In App.vue, replace its content with this:

<template>
  <ul id="todos">
    <li v-for="todo in todos" v-bind:key="todo.id">{{ todo.content }}</li>
  </ul>
</template>

<script>
  export default {
    name: 'app',

    data() {
      return {
        todos: []
      }
    },

    created() {
      fetch("/api/todos")
        .then(res => res.json())
        .then(json => {
          this.todos = json.todos
        })
    }
  }
</script>

This component makes an API request using fetch when created, stores what comes back in the todos array, and uses a v-for loop to display each todo's content. Mirage intercepts the network request based on the routes in src/server.js — no Mirage-specific code is needed in the component itself. When the project is in production and requests go to real backend endpoints that match those routes, nothing has to change.

Restart your Vue dev server via yarn serve and two sample todos should appear. Note that no separate terminal command runs Mirage; it lives inside the Vue application.

Unit Testing with Mirage JS and jest

Mirage JS works nicely with Vue Test Utils to mock network requests in unit tests. Jest is the testing framework here. If you're following along with a Vue CLI project, install the @vue/unit-jest plugin:

vue add @vue/unit-jest

The CLI adds @vue/cli-plugin-unit-jest, @vue/test-utils as development dependencies, creates a tests directory plus a jest.config.js, and inserts this script into the package.json file:

"test:unit": "vue-cli-service test:unit"

Preparing App.vue and the Test File

Update App.vue as follows so it can be tested alongside Mirage's server:

<!-- src/App.vue -->
<template>
  <div v-if="serverError">
    {{ serverError }}
  </div>

  <div v-else-if="todos.length === 0">
    No todos!
  </div>

  <div v-else>
    <ul id="todos">
      <li
        v-for="todo in todos"
        v-bind:key="todo.id"
        :data-testid="'todo-' + todo.id"
      >
        {{ todo.content }}
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    name: "app",

    data() {
      return {
        todos: [],
        serverError: null,
      }
    },

    created() {
      fetch("/api/todos")
        .then(res => res.json())
        .then(json => {
          if (json.error) {
            this.serverError = json.error
          } else {
            this.todos = json.todos
          }
        })
    },
  }
</script>

Instead of the tests directory, create a src/__tests__ folder and an App.spec.js file inside it, which is the recommended Jest practice.

Wire up the test with the necessary imports:

// src/__tests__/App.spec.js
import { mount } from "@vue/test-utils"
import { makeServer } from "../server"
import App from "../App.vue"

let server

beforeEach(() => {
  server = makeServer({ environment: "test" })
})

afterEach(() => {
  server.shutdown()
})

Use the beforeEach hook to start the Mirage server in the test environment, and stop it in afterEach using server.shutdown.

Running the Tests

Complete the test file with the mounting logic and assertions:

// src/__tests__/App.spec.js

import { mount } from "@vue/test-utils"
import { makeServer } from "./server"
import App from "./App.vue"

let server

beforeEach(() => {
  server = makeServer({ environment: "test" })
})

it("shows the todos from our server", async () => {
  server.create("todo", { id: 1, content: "Learn Mirage JS" })
  server.create("todo", { id: 2, content: "Integrate with Vue.js" })

  const wrapper = mount(App)

  // let’s wait for our vue component to finish loading data
  // we know it’s done when the data-testid enters the dom.
  await waitFor(wrapper, '[data-testid="todo-1"]')
  await waitFor(wrapper, '[data-testid="todo-2"]')

  expect(wrapper.find('[data-testid="todo-1"]').text()).toBe("Learn Mirage JS")
  expect(wrapper.find('[data-testid="todo-2"]').text()).toBe("Integrate with Vue.js")
})

it("shows a message if there are no todo", async () => {
  // Don’t create any todos

  const wrapper = mount(App)
  await waitFor(wrapper, '[data-testid="no-todos"]')

  expect(wrapper.find('[data-testid="no-todos"]').text()).toBe("No todos!")
})

// This helper method returns a promise that resolves
// once the selector enters the wrapper’s dom.
const waitFor = function(wrapper, selector) {
  return new Promise(resolve => {
    const timer = setInterval(() => {
      const todoEl = wrapper.findAll(selector)
      if (todoEl.length > 0) {
        clearInterval(timer)
        resolve()
      }
    }, 100)
  })
}

afterEach(() => {
  server.shutdown()
})

Note: A helper defined in the Mirage JS docs is used here. It returns a promise that resolves when the tested elements are rendered in the DOM.

Run yarn test:unit; tests should pass.

Testing Different Server States

Server states can be altered to test varied responses. Import the Response class from Mirage, then define an additional test scenario returning a specific state:

// src/__tests__/App.spec.js
import { Response } from "miragejs"
it("handles error responses from the server", async () => {
  // Override Mirage’s route handler for /todos, just for this test
  server.get("/todos", () => {
    return new Response(
      500,
      {},
      {
        error: "The database is taking a break.",
      }
    )
  })

  const wrapper = mount(App)

  await waitFor(wrapper, '[data-testid="server-error"]')

  expect(wrapper.find('[data-testid="server-error"]').text()).toBe(
    "The database is taking a break."
  )
})

Re-run the tests and the new case will pass as well.

Where To Go From Here

Mirage JS solves a real problem: building a polished front end when there is no backend API ready yet. This article covered that core idea and walked through wiring Mirage JS into a Vue.js project, from installation to defining routes and mocking responses.

That setup gives you a working foundation, but Mirage JS has more to offer. For deeper dives, the official documentation covers everything from factories and serializers to testing. You can also ask questions and share setups in the Mirage JS Discord server.

The complete supporting repository for this walkthrough is on GitHub if you want to compare implementations or start from a working example.

Reference Material

If you want to keep exploring frontend workflows and tooling, the following articles offer useful perspectives:

Smashing Editorial