Readers like you help support MUO. When you make a purchase using links on our site, we may earn an affiliate commission. Read More.

When developing full-stack applications, a significant chunk of the frontend work relies on real-time data from the backend.

This can mean that you must hold off on developing the user interface until the API is available for use. However, waiting for the API to be ready to set up the frontend can considerably reduce productivity and extend project timelines.

A great workaround to this challenge involves utilizing mock APIs. These APIs allow you to develop and test your frontend using data that mimics the structure of the real data, all without relying on the actual API.

Getting Started With Mirage.js Mock APIs

Mirage.js is a JavaScript library that lets you create mock APIs, complete with a test server running on the client side of your web application. This means you can test your frontend code without needing to worry about the availability or behavior of your real backend API.

A laptop kept on a desk with code open on the screen.

To use Mirage.js, you first need to create mock API endpoints and define the responses that they should return. Then, Mirage.js intercepts all HTTP requests that your frontend code makes and returns the mock responses instead.

Once your API is ready, you can easily switch to using it by only changing the configuration of Mirage.js.

You can find this project's source code in this GitHub repository.

Create a Mock API Server With Mirage.js

To demonstrate how to set up mock APIs, you'll build a simple to-do React app that uses a Mirage.js backend. But first, create a React application using the create-react-app command. Alternatively, you can use Vite to set up a React project. Next, install Mirage.js dependency.

 npm install --save-dev miragejs 

Now, to create a Mirage.js server instance to intercept requests and mock API responses, use the createServer method. This method takes a configuration object as a parameter.

This object includes the environment and namespace for the API. The environment specifies the stage of development that the API is at such as development while the namespace is the prefix added to all API endpoints.

Create a new src/server.js file and include the following code:

 import { createServer, Model } from 'miragejs';

const DEFAULT_CONFIG = {
  environment: "development",
  namespace: "api",
};

export function makeServer({ environment, namespace } =
  DEFAULT_CONFIG) {
   let server = createServer({
      environment,
      namespace,
      models: {
      Todo: Model,
    },
  });

  return server;
}

If needed, you can customize the namespace to match the URL structure of your actual API, including specifying the version. This way, once your API is ready, you can easily integrate it into your front-end application with minimal code changes.

In addition, within the server instance configuration, you can also define a data model to simulate data storage and retrieval in the mock environment.

Finally, start the Mirage.js server by importing the server object in your index.jsx or main.jsx file as follows:

 import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { makeServer } from './server';

if ( process.env.NODE_ENV === 'development' &&
     typeof makeServer === 'function'
   ) {
  makeServer();}

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Add Seed Data to the Mock API

Mirage.js has an in-memory database that you can use to prepopulate the mock API with initial seed data and to manage test data from your client application. This means you can store and fetch the test data from the mock database and use it in your client application.

To add seed data to the Mock API, add the following code in the server.js file right below the models object.

 seeds(server) {
      server.create('Todo', {
        title: 'item no 1',
        body:
          'Do something nice for someone I care about',
      });
      server.create('Todo', {
        title: 'item no 2',
        body:
          'Memorize the fifty states and their capitals.',
      });
      server.create('Todo', {
        title: 'item no 3',
        body:
          'Watch a classic movie.',
      });
    },

The seeds function populates a Mirage.js server with three to-do items, each with a title and description. Optionally, instead of hard coding the test data, you can integrate a library such as Faker.js to generate the required test data.

Define the Mock API Routes

Now, define some API routes for the mock API. In this case, specify routes to handle GET, POST, and DELETE mock API requests.

Right below the seed data, add the code below:

 routes() {
      this.namespace = 'api/todos';
      
      this.get('/', (schema, request) => {
        return schema.all('Todo');
      });

      this.post('/', (schema, request) => {
        let attrs = JSON.parse(request.requestBody);
        return schema.create('Todo', attrs);
      });

      this.delete('/:id', (schema, request) => {
        let id = request.params.id;
        return schema.find('Todo', id).destroy();
      });
    }

Build a React Client

Now that the mock API is set up, let's build a React client to interact with and consume the API endpoints. You are free to use any UI component library you like, but this guide will use Chakra UI to style the app.

First, install these dependencies:

 npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion 

Next, create a new src/components/TodoList.jsx file, and include the following code:

 import React, { useState, useEffect } from 'react';
import {
  Button,
  Box,
  Container,
  Text,
  Input,
  FormControl,
  Flex,
} from '@chakra-ui/react';

Now, define a functional component to render the to-do list user interface, including the input fields for adding new tasks and a list of existing tasks.

 export default function TodoList() {
  return (
    <Container>
      <Text fontSize="xl" mb={4}>Todo List</Text>
      <FormControl mb={4}>
        <Input
          type="text"
          name="body"
          value={newTodo.body}
          onChange={handleInputChange}
        />
      </FormControl>
      <Button colorScheme="teal" onClick={handleAddTodo}> Add Todo</Button>
      {loading ? ( <Text>Loading...</Text> ) : (
        todos.map((todo) => (
          <Box key={todo.id} mb={2} p={2} borderWidth="1px">
            <Flex align="center">
              <Text flex="1">{todo.body}</Text>
              <Button
                colorScheme="red"
                size="sm"
                onClick={() => handleDelete(todo.id)}>Delete
              </Button>
            </Flex>
          </Box>
        ))
      )}
    </Container>
  );
}

Now, define the handler functions for the add and delete operations. But first, add these states. Alternatively, you can use the useReducer hook to define the state management logic for the to-do list app.

 const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState({ title: '', body: '' });
const [loading, setLoading] = useState(true);
const [renderKey, setRenderKey] = useState(0);

Now, define the logic to retrieve and display the seed data in the in-memory database when the application first loads in the browser by wrapping the fetch method in a useEffect hook.

  useEffect(() => {
    fetch('/api/todos')
      .then((response) => response.json())
      .then((data) => {
        setTodos(data.todos);
        setLoading(false);
      });
  }, [renderKey]);

The renderKey state is also included in the useEffect to ensure that the code triggers a re-render of newly added data in the in-memory database when the server is running.

Simply put, whenever a user adds new to-do data to the Mirage.js database—the component will re-render to display the updated data.

Adding Data to the API

Now, define the logic for adding data to the API through POST requests. Right below the useEffect hook, include the following code.

 const handleInputChange = (e) => {
  const { name, value } = e.target;
  setNewTodo((prevTodo) => ({ ...prevTodo, [name]: value }));
};

const handleAddTodo = () => {
  setLoading(true);
  fetch('/api/todos', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newTodo),
  }).then((response) => response.json()).then((createdTodo) => {
      setTodos((prevTodos) => [createdTodo, ...prevTodos]);
      setNewTodo({ title: '', body: '' });
      setRenderKey((prevKey) => prevKey + 1);
      setLoading(false);
    }).catch((error) => {
      console.error('Error adding todo:', error);
      setLoading(false);
    });
};

When a user enters data in the to-do input field and clicks the Add Todo button, the code updates the newTodo state with the user's input. Then, it sends a mock POST request to the API with the new data object in the request body to save it to the in-memory database.

If the POST request is successful, the code adds the new item to the todos array, and finally, triggers the component re-render to show the new to-do item.

Mock API DELETE Requests

Now, define the logic for deleting data through DELETE mock API requests. This process involves sending a DELETE request to remove the to-do item from the in-memory database. If successful, update both the todos and loading state to reflect the deletion process.

 const handleDelete = (id) => { 
    let deleteInProgress = true;
    fetch(`/api/todos/${id}`, {
      method: 'DELETE',
    }).then((response) => {
        if (response.status === 204) {
          return null;
        } else {
          return response.json();
        }
      }) .then((data) => {
        if (data && data.error) {
          console.error('Error deleting todo:', data.error);
        } else {
          setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
          setRenderKey((prevKey) => prevKey + 1);
        }
        deleteInProgress = false;
      }).catch((error) => {
        console.error('Error deleting todo:', error);
        deleteInProgress = false;
      }) .finally(() => {
        setLoading(deleteInProgress);
      });
  };

Keep in mind that this process can only delete newly added data, not the seed data.

Finally, import the TodoList component in the App.jsx file to render it in the DOM.

 import TodoList from './components/TodoList';
//code ...
<TodoList />

Great! Once you start the development server, you can fetch the seed data, and add and delete new data from the mock API in your React app.

Using Mock APIs to Speed Up Development

Mocking APIs is a great way to accelerate frontend development, whether you are working on a project individually or as part of a team. By using Mock APIs, you can quickly build the UI and test their code without waiting for the backend to be complete.