Building full-stack applications can be challenging, especially when developing the backend and frontend at the same time. In this scenario, frontend teams may have to wait for the backend team to finish building an API before they implement. This is where Mirage.js comes in. In this tutorial, you will explore how to use Mirage.js in frontend applications and mock backend requests for services that have not yet been developed. You will use an existing frontend application that does not have any API configuration.

Prerequisites

To follow along, you’ll need the following:

  • A GitHub account
  • A CircleCI account
  • NodeJS installed locally
  • Knowledge of CI/CD (Continuous Integration and Continuous Deployment / Delivery)
  • Basic understanding of JavaScript, Jest, and unit testing.

Setting up your application

To speed things along, you will use an existing application instead of creating a new one. Clone and set up the application using the following commands.

Clone the repository:

git clone https://github.com/CIRCLECI-GWP/mocking-apis-with-mirage.git
# cd into the cloned directory:

cd mocking-apis-with-mirage

Install dependencies:

npm install

What is Mirage?

Mirage is a library that creates proxy API requests similar to those you would make with a real API. A proxy intercepts requests and provides a mock request similar to what would be received in an actual API call. Mirage doesn’t modify the frontend logic as it interacts with the HTTP API layer. That means there are no ramifications when you switch the Mirage APIs with an actual backend. The most significant advantage of Mirage is that you can continue working on the backend using mocked requests while APIs are still being developed. This can result in significant time savings.

There are other benefits to using Mirage:

  • Mocking an API with Mirage saves development time, especially for frontend teams
  • Mocking with Mirage offers faster response times than real data from an API
  • Mocks provide insights into what an actual API call will look like and can help design data flows and API structures

Mock response

Configuring Mirage

In this section, you will work on a basic configuration of Mirage in an application.

Run this command to view the application you cloned earlier:

npm start

Note: The application starts running on the URL http://localhost:3000, and can be viewed using any browser. :3000 is the default port. You can change that to any port that is free on your machine.

Executing our application

For now, we do not have any Todo items on the page. Let’s see how we can get our todos to display.

The changes to this application can be found in the dev branch of the repository.

Configuring the server.js file

In the server.js file we need to import Server from miragejs, a dependency that we installed before. We then define our request methods within the routes() hook. We provide the methods with a URL and a function that returns a response. Copy this code into the server.js file:

import { Server } from 'miragejs';

let todos = [
    { id: 1, name: "Groom the cat" },
    { id: 2, name: "Do the dishes" },
    { id: 3, name: "Go shopping" }
]

export function makeServer() {
    let server = new Server({
        routes() {
            // GET REQUEST
            this.get("/api/todos", () => {
                return {
                    todos
                }
            })
        }
    })

    return server
}

The GET request returns all your todos. The todos should now display on the application page.

Todo items

The next request is a POST request, which makes it possible to add new todo items to the list. Copy this code snippet and add it to the routes() hook below the GET request:

            // POST REQUEST
            this.post("/api/todos", (schema, request) => {
                const attrs = JSON.parse(request.requestBody)
                attrs.id = Math.floor(Math.random() * 1000)
                todos.push(attrs)

                return { todo: attrs }
            })

The first argument for this handler is the URL. Then, the callback function provides schema, used for the data layer, and request to access properties of the request object. The request body is saved as the name of the todo you want to add to the attrs variable. The todo is then given a random id and gets pushed to the todos array. The return statement provides the frontend access to the new todo. You can now add it to the todos state.

Add a new todo by typing it and clicking the + button.

Adding Todo items

The next thing to work on is the DELETE request. Add this code snippet below the previous requests:

            // DELETE TODO
            this.delete("/api/todos/:id", (schema, request) => {
                const id = request.params.id
                return schema.todos.find(id).destroy()
            })

This code snippet uses a dynamic route because you are deleting a specific todo. You get its id and then remove the todo from the schema by using the destroy() method. There are a variety of methods that perform different operations. In the browser window, click the delete button for a todo to delete it.

Seeding data

In the previous code snippets, you have been hardcoding most of the data. Fortunately, Mirage provides a way for you to create the objects for the todos using the seeds() hook. Todos are also assigned incremental IDs when they are created so you don’t have to include them.

Create a todo model to access through the schema. Use this code snippet:

        models: {
            todo: Model
        },

        seeds(server) {
            server.create("todo", { name: "Groom the cat" })
            server.create("todo", { name: "Do the dishes" })
            server.create("todo", { name: "Go shopping" })
        },

The next step is configuring the application to access the todos. Mirage offers some methods such as all() and create(), that you can use to access and manipulate the data layer. The server.js file should now look like this:

import { Server, Model } from 'miragejs';

export function makeServer({ environment = 'development' } = {}) {
    let server = new Server({
        environment,

        models: {
            todo: Model
        },

        seeds(server) {
            server.create("todo", { name: "Groom the cat" });
            server.create("todo", { name: "Do the dishes" });
            server.create("todo", { name: "Go shopping" });
        },

        routes() {
            // GET REQUEST
            this.get("/api/todos", (schema, request) => {
                return schema.todos.all()
            })

            // POST REQUEST
            this.post("/api/todos", (schema, request) => {
                const attrs = JSON.parse(request.requestBody)

                return schema.todos.create(attrs)
            })

            //DELETE TODO
            this.delete("/api/todos/:id", (schema, request) => {
                const id = request.params.id

                return schema.todos.find(id).destroy()
            })
        }
    })
    return server
}

The seeds() hook provides initial data to the todo model, which you access inside the routes using different methods. Click here to learn more about the schema argument and how you can use it to interact with the Object Relational Mapper.

Go back to the browser to review todos, add a new one, or delete a todo.

Writing tests using Mirage

With Mirage, you can use the server for both development and testing purposes without having to worry about duplication, because it is not environment-specific.

Update your App.test.js file to look like this:

import {
  render,
  screen,
  waitForElementToBeRemoved,
  fireEvent
} from '@testing-library/react';
import '@testing-library/jest-dom'
import userEvent from "@testing-library/user-event";
import App from './App';
import { makeServer } from './server'

let server;

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

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

test('Page loads successfully', async () => {
  render(<App />);

  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
  expect(screen.getByText("Todos")).toBeInTheDocument()

});

test('Initial todos are displayed', async () => {
  server.create("todo", { name: "Grooming the cat" })
  render(<App />);
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("Grooming the cat")).toBeInTheDocument()

})

test('Todo can be created', async () => {
  render(<App />);
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  const postTodo = await screen.findByTestId("post-todo")
  userEvent.type(postTodo.querySelector("input[type=text]"), "Feed the cat")
  fireEvent.submit(screen.getByTestId("post-todo"))
})

This code snippet imports the makeServer() function that contains our server and, instead of calling it inside every test, you call it in the beforeEach() method provided by Jest. Then, clean up using the afterEach() method.

Writing tests for Mirage functionality includes adding assertions for each test. If the validations are correct, the tests will pass.

 PASS  src/App.test.js
  ✓ Page loads successfully (619 ms)
  ✓ Initial todos are displayed (439 ms)
  ✓ Todo can be created (464 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        4.782 s
Ran all test suites.

Tests are not complete if they aren’t shared, right? To share your tests, hook them up to a CI/CD pipeline.

Setting up CircleCI

CircleCI is a CI/CD tool that automates workflows and runs tests (and much more).

Create a .circleci folder at the root of your project and place a config.yml file inside it. The config.yml file determines the execution of your pipeline. Add this to it:

version: 2.1
orbs:
  node: circleci/node@4.7
jobs:
  build-and-test:
    docker:
      - image: cimg/node:16.10     
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: npm
      - run:
          name: Run tests
          command: npm test

workflows:

  sample:
    jobs:
      - build-and-test
      - node/test

This code snippet installs the required dependencies, saves the cache for faster builds, and then runs your tests.

Commit and push our local changes to GitHub. You can find the repository as it should be at this point here.

Note: GitLab users can also follow this tutorial by pushing the sample project to GitLab and setting up a CI/CD pipeline for their GitLab repo.

Next, sign in to your CircleCI account to set up the project. Click Set Up Project next to the name of your remote repository.

Setting up project

Select fastest then click Set Up Project.

Selecting configuration file

The build job will then be run by CircleCI, and if everything is okay you will have a green build.

Running CircleCI pipeline

Conclusion

In this tutorial, you have learned how to use Mirage when you need a backend to simulate live API calls to the server.

You also learned how to configure Mirage to behave like a server that you can make requests to when you work with an existing frontend application. You used the seeds() hook to create initial data for the server with default incremental IDs and you wrote tests for Mirage. Now you don’t have to wait for backend applications to be developed before commencing frontend work. Until the next one, happy mocking!


Waweru Mwaura is a software engineer and a life-long learner who specializes in quality engineering. He is an author at Packt and enjoys reading about engineering, finance, and technology. You can read more about him on his web profile.

Read more posts by Waweru Mwaura