Photo credits bermixstudio
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);
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;
}
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>
</>
);
}
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;
};
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.