Creating independent front-ends for any application leads to easily maintainable and flexible code but, for creating such independent front-ends, one has to mock the complete API so that the front-end has data and handles all the requests. The process of mocking the API and data can get tedious. Using API mocking libraries can help reduce the time of creating a full-fledged front-end that does not depend on any back-end services.
One such API mocking library is Mirage. In this article, we are going to use Mirage and create a full-fledged React application that does not rely on any back-end service. We are going to create a REST API for the following application:
It is a book store application where we can add books along with the genre of the books. It also displays our stored book list. The book list is displayed based on two categories, one is the complete list of books stored, the other is the list of books based on genre.
I have provided the link to the starter code which includes all the basic setup that is required for this application:
https://github.com/Bviveksingh/mirage-starter-code
After cloning or downloading the starter-code, run `npm install` in the terminal to install all the dependencies. After installing the dependencies, run the application using `npm start`command. This is how the application look with the starter code:
Try clicking on the show all books button, you should see an error in the console, Mirage error. This is because our application is currently sending HTTP requests to an endpoint which we haven’t coded yet. So let’s start building the API,
We have to install Mirage to our application first, so run the following command to install mirage:
npm i --save-dev miragejs
Next, inside the components folder, create a file named server.js and add the following code inside the file:
import { createServer } from "miragejs" export default function () { createServer() }
The createServer( ) function creates the server for us. All the routes,models and mock data will go inside this function. If you want to use the Mirage server only during the development phase, inside the index.js file, add the following code:
import createServer from "./server"; if (process.env.NODE_ENV === "development") { createServer(); }
By writing the above code, the mirage server will run only in the development phase.
Now that we have completed setting up the Mirage server, let’s move onto creating models for the API. We are going to create two models, one for storing books and the other for storing genres.
Inside the createServer function, add the following code:
createServer({ models:{ book:Model, genre:Model } })
The models property inside the createServer function specifies the number of Models this server consists of. When using POST requests, Mirage uses it’s in-memory database to store our application data.
**Note– Don’t forget to import Model from miragejs.
Now that we have created models, let’s move on to creating endpoints for these models. For this application we are going to use the following endpoints:
Inside the createServer function, add another property (a method in this case) called routes( ). This method will contain all the endpoints required for the application.
createServer({ models:{ book:Model, genre:Model }, routes(){ } })
All the endpoints should be written inside the routes function. Let’s add the first endpoint for creating a book:
this.post('/api/add_book',(schema,request)=>{ let body = JSON.parse(request.requestBody); return schema.books.create(body); });
Just like creating endpoints for REST API, the above POST request takes in two parameters, first one is the name of the endpoint and the second is a callback function. The callback function has two parameters: schema (for accessing the data layer of Mirage) and request (to access the request body and params). We can access the book Model using schema.books, for creating new data we use the create method.
Mirage automatically creates documents with incrementing ids so we don’t have to worry about each document having a unique id. For example, consider adding 3 books to the database, this is how they will be represented:
name:"Book1", id:"1" name:"Book2",id:"2" name:"Book3", id:"3"
Now run the application again, try adding a new book by clicking the add book button. You should see 201 status code for the Mirage request inside the console window. Next, for getting the list of all books stored along with deleting a specific book add the following endpoints:
this.get('/api/books',(schema)=>{ return schema.books.all(); }); this.delete('/api/book/:id',(schema,request)=>{ let id = request.params.id; return schema.books.find(id).destroy(); });
The above two endpoints are pretty straight forward, the GET request endpoint returns the complete list of books from the in-memory database using the all( ) method. Then, the DELETE request endpoint is a dynamic endpoint, which takes an id as the parameter, inside the callback function,we use the destroy( ) method to delete the document from the database.
To check whether the application is working, try adding a new book and then click the show all books button, you should see the list of books appearing inside the books section:
The cover page of all the books will be a static image which we are loading from the images folder. You can go ahead and change it. As you can see we are now able to add a new book to the Mirage database. Next, for checking the DELETE endpoint, try deleting the a book from the list by clicking the delete button on the book item. After clicking the book item, click on the show all books button again you should no longer see the book you deleted.
There are two other end points that we need to add to our server, the GET all genres endpoint and the GET all books of specific genre endpoint:
this.get('/api/genres',(schema,request)=>{ return schema.genres.all(); });
****We are going to add the GET all books of specific genre end-point when we discuss relational databases.
So right now, our application works and we are able to add/get/delete books from the Mirage database, the point here to notice is that every time we reload the application, we receive an empty list of books. This is because the Mirage server reruns when the application gets reloaded and therefore the list is empty.What if we want to load some initial mock data so that our book list is not empty when we start the application? For doing so, Mirage provides a method called seeds inside of which we add all the initial mock data which can be used in the application.
Other than loading initial books, if you observe the application, when clicking on the Group by Genre button, we don’t see any list appearing. The list contains all the genres that have been created in the database but, we have not added any genre to the database yet.
So let’s add some initial books and genres to the database. Add the following method inside the createServer function:
seeds(server){ server.create("genre",{name:"Mystery"}); server.create("genre",{name:"Thriller"}); server.create("genre",{name:"Sci-fi"}); server.create("book",{name:"Book1"}); server.create("book",{name:"Book2"}); server.create("book",{name:"Book3"}); server.create("book",{name:"Book4"}); }
As one can see in the code above, using the server.create method, we create both initial books and genres. Now when you run the application and click the show all books button, you should see all the 4 books appearing in the list. Along with this, you can also see the list of genres by clicking the group by genre button:
So we have completed the basic operations that are performed in the back-end but, we can see that nothing really happens when you click on one of the items inside the Group by Genre list. This is where relational data comes into picture and we’re going to learn that next.
We have created models that are not related to each other. What if we want the models to have some kind of relationship? Mirage provides a way to add relationships between models. Let’s take a look at how that is possible.
We will create a one-to-many relationship between the book and genre model i.e each genre can be related to many books and every book is related to one genre. This way we are creating a one-to-many relationship. Let’s write code for that. Inside the models property of the createServer function, make the following changes:
models:{ book:Model.extend({ genre:belongsTo() }), genre:Model.extend({ books:hasMany() }) }
In the above code, by adding belongsTo and hasMany methods, we are specifying the server that the book and genre models maintain a one-to-many relationship. Now the server knows that book.genre and genre.books properties are relationships between the models.
**Note- Don’t forget to import belongsTo and hasMany methods from ‘miragejs’
Now that we have specified relationships, let’s add some initial mock data:
let mysteryGenre = server.create("genre",{name:"Mystery"}); let thrillerGenre = server.create("genre",{name:"Thriller"}); let sciFiGenre = server.create("genre",{name:"Sci-fi"}); server.create("book", {name:"Book5", genre:mysteryGenre}); server.create("book",{name:"Book6",genre:thrillerGenre}); server.create("book",{name:"Book7",genre:sciFiGenre});
Until now, we have created books only using the name property but as one can see in the above code, we are now creating new books having the genre property attached to it. Mirage understands that this property is a relationship and hence all the books from now will be saved with the following properties in the database:
name:"Book5",id:"4", genreId:"1"
We can now add the end-point for displaying all the books related to a particular genre:
this.get('/api/genre/:id/books',(schema,request)=>{ let genreId = request.params.id; return schema.genres.find(genreId).books; });
In the above end-point we are first searching for the genre with the particular id and then finding all books related to that genre. Now, try clicking on one of the items inside the Group by Genre list you should the specific book related to that genre appear in the books section:
So there you go, we have created an API which loads initial data, contains endpoints, performs most of the CRUD operations and also contains models. Did you see how easy it was to create a fully independent front-end application? Go ahead and try creating some awesome independent front-ends.
For the live demo, checkout the link below:
https://codesandbox.io/s/mirage-react-o7j6l?file=/src/App.js
One problem I found with Mirage is there’s no built in pagination support.
Yes, I agree. In general, the basic setup and handling the endpoints gets done very easily with Mirage and not to forget, the data layer of Mirage. Something like “json-server” is quite useful when you are looking for pagination support out of the box.