How to create fake API server for react apps with MirageJS

kpunith8

Punith K

Posted on October 16, 2020

How to create fake API server for react apps with MirageJS

If you are building a web app using React, Vue, Angular, or with any of your favorite front end framework, you need to talk to backend APIs for CRUD operations. Let's say you want to build a prototype of the app quickly, but you don't have the backend APIs ready yet, what will do in this case? The best way is to have mock data from a fake server.

How to create mock data, we have so many libraries that can help us achieve this goal, but in this post, I'm considering using miragejs with React.

Why am I considering this while there are other popular libraries to consider, because of 2 reasons, the first one, you don't have to create/spin another server to load your data for eg: http://localhost:3001 where your mock server runs, but mirage runs in the same development server and lets you access the data like you are working with real APIs, and the second one, you can use the mirage as your API endpoint to write end-to-end tests using Cypress, I didn't even think about other options when I get 2 benefits just creating a mock server with mirage and it offers a great developer experience in my opinion.

You can use it to mock your API endpoints with react-testing-library for writing unit test cases too. Please refer the documentation for more details.

Let's get started, create a react app using create-react-app, and add this to index.js. Runs the mock server only during development.

// index.js
import React from "react";
import ReactDOM from "react-dom";

import { makeServer } from "./server";
import UsersLayout from "./users-layout";

// It creates the mock server only in development mode
if (process.env.NODE_ENV === "development") {
  makeServer({ environment: "development" });
}

const App = () => <UsersLayout />;

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Enter fullscreen mode Exit fullscreen mode

Create server.js where the real magic happens with less code,

// server.js
import { createServer, Model } from "miragejs";

export function makeServer({ environment = "test" } = {}) {
  let server = createServer({
    environment,

    models: {
      user: Model,
    },

    seeds(server) {
      server.create("user", { id: 1, name: "Bob Jhon" });
      server.create("user", { id: 2, name: "Alice" });
    },

    routes() {
      this.namespace = "api";

      this.get("/users", (schema) => schema.users.all());

      // To increment the id for each user inserted,
      // Mirage auto creates an id as string if you don't pass one
      let newId = 3
      this.post("/users", (schema, request) => {
        const attrs = JSON.parse(request.requestBody);
        attrs.id = newId++

        return schema.users.create(attrs);
      });

      this.delete("/users/:id", (schema, request) => {
        const id = request.params.id;

        return schema.users.find(id).destroy();
      });
    },
  });

  return server;
}
Enter fullscreen mode Exit fullscreen mode

seeds() method will seed our user model with some initial data so that we can start using it immediately, you can leave it empty if you want to start with an empty user collection.

Define all your API routes in the routes() method and you can define your API namespace with this.namespace = 'api' so that you don't have to repeat it in all the routes like for eg: this.get('/api/users'). Here I've three routes to GET, POST, and DELETE a user.

You need to create a model with the help of mirage Model and with that, you can access data from schema, if you notice carefully, I've created a user model with the name user but accessing it as schema.users.all(), mirage does create the pluralized collection for us looking into the name of the model, its good practice to keep singular names for your models.

Mirage offers other methods on the schema to add and delete an item from the collection, see delete, and post API routes in the code example above.

That's it, let us write React side of the code so that we can consume the mirage's fake API with fetch or axios, I'm using fetch here.

// users-layout.js
import React, { useState, useEffect, useCallback } from "react";
import { useFetch } from "./use-fetch";

export default function UsersLayout() {
  const [users, setUsers] = useState([]);
  const { data, loading: userLoading, error: userError } = useFetch(
    "/api/users"
  );
  const [name, setName] = useState("");
  const [isUpdating, setIsUpdating] = useState(false);

  useEffect(() => {
    if (data) {
      setUsers(data.users);
    }
  }, [data]);

  const onAddUser = useCallback(
    async (e) => {
      e.preventDefault();
      try {
        setIsUpdating(true);
        const res = await fetch("/api/users", {
          method: "POST",
          body: JSON.stringify({ name }),
        });

        const data = await res.json();
        setUsers((users) => users.concat(data.user));
        setIsUpdating(false);
        setName("");
      } catch (error) {
        throw error;
      }
    },
    [name]
  );

  return (
    <>
      <form onSubmit={onAddUser}>
        <input
          type="text"
          onChange={(e) => setName(e.target.value)}
          value={name}
        />
        <button type="submit" disabled={isUpdating}>
          {isUpdating ? "Updating..." : "Add User"}
        </button>
      </form>
      {userError && <div>{userError.message}</div>}
      <ul>
        {!userLoading &&
          users.map((user) => <li key={user.id}>{user.name}</li>)}
      </ul>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

And a bonus in the above code, I wrote a custom hook to fetch the data useFetch from any API endpoints. Let's look at the code for useFetch

// use-fetch.js
import { useEffect, useState, useRef } from "react";

/**
 * Hook to fetch data from any API endpoints
 */
export const useFetch = (url) => {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null,
  });
  const isCurrent = useRef(true);

  useEffect(() => {
    return () => {
      isCurrent.current = false;
    };
  }, []);

  useEffect(() => {
    setState((state) => ({ ...state, loading: true }));
    const getData = async () => {
      try {
        const res = await fetch(url);
        const data = await res.json();

        // If calling component unmounts before the data is
        // fetched, then there is a warning, "Can't perform
        // React state update on an unmounted component"
        // it may introduce side-effects, to avoid this, useRef to
        // check for current reference.
        if (isCurrent.current) {
          setState((state) => ({
            ...state,
            data,
            loading: false,
            error: null,
          }));
        }
      } catch (error) {
        setState((state) => ({ ...state, error: error }));
      }
    };

    getData();
  }, [url]);

  return state;
};
Enter fullscreen mode Exit fullscreen mode

That's it, with a little effort you are able to mock the data with a fake API server using miragejs. And mirage scales well with large applications too, I've battle-tested this and hope you will find it useful. Give a try on your next project. This is going to save a lot of time during development.

I'll write a follow-up article on how I used miragejs as a backend for Cypress end-to-end tests, until then bye, bye.

💖 💪 🙅 🚩
kpunith8
Punith K

Posted on October 16, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related