Because you may be developing the backend and frontend at the same time, building full-stack applications can be quite a challenge. Before they can implement, front-end teams may have to wait for the back-end team to finish building the API. This is where Mirage.js comes in. In this tutorial, you will learn how to use Mirage.js
in front-end applications and mock back-end requests for services that have not yet been developed. You will use an existing front-end 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 -b dev https://github.com/CIRCLECI-GWP/mocking-api-requests-mirage.git
This is the dev
branch of the repository. It contains enough code to get you started. You will build on it as you follow the steps of the tutorial.
# cd into the cloned directory:
cd mocking-api-requests-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 front-end 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 front-end 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
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. The default port is :3000. You can change that to any port that is free on your machine.
For now, there are no Todo items on the page, so you need to get them to display.
Configuring the server.js file
In the server.js
file, import Server
from miragejs
, a dependency that you installed before. Then define your request methods within the routes()
hook. You 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.
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. 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 front-end access to the new todo. You can now add it to the todo’s state.
Add a new todo by typing it and clicking the + button.
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. 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
Because Mirage is not environment-specific, you can use the server for both development and testing without worrying about duplication.
Update your App.test.js
file to 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 your server and, instead of calling it inside every test, calls it in the beforeEach()
method provided by Jest. Then it cleans 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 (36 ms)
✓ Initial todos are displayed (11 ms)
✓ Todo can be created (26 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.024 s
Ran all test suites related to changed files.
Tests are not complete if they aren’t shared, right? The best way to share your tests is to hook them up to a CI/CD pipeline.
Setting up CircleCI
CircleCI is a CI/CD tool that automates workflows, runs tests, and much more.
Create a .circleci
folder at the root of your project and add a config.yml
file to it. The config.yml
file determines the execution of your pipeline. Add this:
version: 2.1
orbs:
node: circleci/node@6.3.0
jobs:
build-and-test:
docker:
- image: cimg/node:23.0.0
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 your 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.
Select fastest
then click Set Up Project.
The build job will then be run by CircleCI, and if everything is okay you will have a green build.
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 front-end 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 back-end applications to be developed before you can begin front-end work.
Until next time, happy mocking!