Fullstack CRUD application using Fastify, React, Redux & MongoDB Part-2
Let’s create something interesting
Hi there, This tutorial is the final installment of the my two part tutorial series, in which we are building a Fullstack Application using Fastify, React, Redux and MongoDB, which would support all CRUD functionalities. In case you didn’t read the first part, I’ll highly recommend you to do that before you move any forward because this tutorial would be the continuation of Part 1 and in order to understand what’s going on you are expected to have at least some basic understanding of our frontend that we made in Part 1. You don’t need to know meaning of every single line we wrote there. You just need to know about how the data is flowing across the app and what event handlers are making things as they are. A quick skim of the blog will do the job. File structure was discussed early on in the Part 1 blog, So you just need to create clone of that file structure and copy the respective codes in the their corresponding files and you are good to go. You can read Part-1 from here.
So, now that you have skimmed the blog and have the code of React Redux app that we built in Part 1. I expect you to have some basic understanding of the current code base. So, if you played with the incomplete version of the app that we built in part 1, you may have realized that it lacks persistence to changes. Our redux store is wiped out every time we reload the application. So, in this Part of tutorial series we’ll be making our application persistent to changes. As a bonus section I’ll also guide you over deployment process. So without wasting any time, Let’s get started!
Project Setup for Server
If you remember from Part 1, we already have a project setup. We just need to fill up our vacant server directory with some server related files. so let’s get this done.
Navigate to server/
> Open your terminal > execute the commands shown below.
touch controller.js routes.js server.js
mkdir models
cd models && touch menuItems.js
cd ../../ && touch .env
Open .env
and paste couple of lines given below
NODE_ENV=development
MONGO_ATLAS_PW=<USER_PASSWORD>
Now you must be having your file structure inside server directory quite similar to as shown below
|server
|--models
|----menuItems.js // contains schema
|--controller.js // contains controllers
|--routes.js // contains routes
|--server.js // contains fastify server
|.env // contains user defined env variables
Now let’s install some node modules that we’ll require for this part of the tutorial
npm i --save axios dotenv fastify fastify-static mongoose
After getting required modules installed we are ready to write some code. We’ll start by building backend api using fastify + MongoDB and then in later half of the tutorial we’ll spending time in connecting our fastify backend api to our react redux frontend.
Backend API
In this section We’ll create Mongoose Schemas, controllers, routes, and then finally our fastify.js server. Let’s kick off with building our mongoose schema.
Step 1: Open server/models/menuItems.js
and copy the code shown below.
So that’s how our mongoose schema looks like. Here we are having an id
property which stores the id that we create with the help of uuid.v4()
in our <Menu/>
component. We also have name
and price
which would store name and price of the item respectively. After creating schema we create a model which is then exported so that other files like controller.js
could make use of it. Next up we’ll create controllers.
Step 2: Open server/controller.js
and copy the code shown below.
So that’s our controllers. Controllers are basically not so different from event handlers that we use in react. It’s just that their way of getting triggered is different. Each controller defined above would be attached to a route and whenever that particular route will be requested, the responsible controller will get invoked. Here we are avoiding chaining of promises by making use of async
and await
. In case you never heard these terms before, you can visit here to know more about them. Now let’s discuss all the controllers one by one.
fetchItems
: As name implies it’s used to fetch all the items present in database. It takes benefit offind()
API provided by mongoose. Here we don’t pass any argument to it, that’s why it returns every single item present in database.addItem
: It’s used to add a new Item to database. Here we first create a new Item by creating a new instance ofMenuItem
model. Content for new item is received viareq.body
. After creating new item, we add it to database usingsave()
API provided by mongoose.updateItem
: It’s used to update item. Here we usefindOneAndUpdate()
API provided by mongoose to find item with the id =req.params.id
and update it’s content, which we receive viareq.body
.deleteItem
: It’s used to delete a certain item whoseid
matches withid
that it receives as argument. It takes help offindOneAndUpdate()
API provided by mongoose.
Note : In case you are curious that why we are not using findByIdAndUpdate()
and findByIdAndDelete()
as we are doing of all manipulations in data using id
, then you should know that they only work with _id
(which mongoose generates automatically every time an item is created). And the id
that we are using is not equal to _id
. The id
that we are using comes from the React frontend and it get’s generated using uuid.v4()
whenever we add item to our redux store. We are using it because along with MongoDB Database we are also managing a redux store. And when we save items to database we don’t fetch the created item simultaneously, instead we save them locally as well in redux store and work with that copy. We do this to provide better user experience and It’s a very popular practice among react developers in which we’re updating the client locally before waiting to hear from the server. It’s also commonly referred to as optimistic updating. So basically our react frontend at the time of creating items is unaware of _id
property till it fetches the item from database which only happens when we load or reload our application. So if we use _id
instead of id
then apparently our redux functionality would break. And I don’t think that requesting data from database every time we make changes in your application would be performance efficient under slow connections. So that’s why apparently we are managing two ids _id
and id
and why we are using findByIdAndUpdate()
and findByIdAndDelete()
. If all that’s confusing to you forget about it and return here after finishing this tutorial, then you’ll understand what I am trying to say exactly.
So that’s all for our controller.js
, Next up we’ll setup routes and make these controllers useful by attaching them to appropriate ones.
Step 3: Open server/routes.js
and copy the code shown below.
So that’s what our routes.js looks like, In this notation we are specifying a method
, url
, and handler
for every individual router. Here we are assigning a appropriate controller to every route as a handler
. Whenever we request any route, the corresponding controller get’s invoked i.e. when we request GET <domain_name>/api/menuItems
, fetchItems
get’s invoked immediately and returns result. Next up we’ll be wrapping up with our backend API by creating our fastify server.
Step 4: Open server/server.js
and copy the code shown below.
So that’s our fastify server. Now before we go any further, we don’t have any database yet. so let’s get this done quickly. For the time being we’ll be using MongoDB Atlas. In case you are new to MongoDB Atlas, please visit there getting started guide to setup the project (and while doing setup don’t load any sample data, we don’t require it). After successfully setting up the project click on connect
button and then select connect your application
and copy the connection string. Now open your server.js
file and locate mongoose.connect
. After finding it replace old connection string with the new one that you just copied in your server.js
. Okay so, if you followed the getting started guide correctly you must have added a user. Now, open .env
file and replace the <USER_PASSWORD>
with your actual user password. Now I think we have the required setup to go ahead.
Now if everything goes right you must be seeing the two log statements shown below in your console when you run node server/server.js.
server running at 3000
MongoDB connected
And if you navigate to http://localhost:3000. You must be seeing {msg : hello, world!}
in your browser as output.
Now let’s discuss our server in detail. So first off we connect to our database. And then we’ve placed a dummy route which currently does nothing special than printing {msg : hello, world!}
but later on it would play an important role as it will be serving our react application. In next instruction we iterate over all of the imported routes (that we created inroutes.js
) and register them with fastify (which in a nutshell means that we’re making fastify aware of routes that we’ve declared elsewhere). After doing it we bind our server to a port using fastify.listen()
So that’s all for our backend API. Now let’s test our API and best tool to do it is Postman. We will be sending our data as raw objects in the body of the request and as parameters. Don’t forget to run server before testing API.
Saving Item to Database
Reading all the items
Updating item
Deleting Item
We now have a fully functional API — but what about the React Frontend? Let’s wire up them together up next! I assure you it would be fun.
Wiring up React with Fastify
Step 1: Serve react app using fastify server
If you see your project directory you’ll notice that there is folder named dist
(provided that you have built react frontend at least once) that we didn’t create. It actually contains compiled form of our react app and it get’s created automatically every time we build our react app using parcel. So in order to host our react application we just need setup the dist
directory as default lookup folder for our server. So that if we’re required to send index.html
from any of our route handler, we can just send index.html
and our server will by default always send index.html
present in dist
. All this can be accomplished using fastify-static
module. Add the code shown below in your server/server.js
to see it in action yourselves.
const DistPath = path.join(__dirname, '..', 'dist')fastify.register(require('fastify-static'), {
root: DistPath,
})
Now let’s get rid of {msg : hello, world!}
and host our react app on /
route instead. It can be done very easily. We just need to replacereturn {msg: hello, world!
with reply.sendFile('index.html')
Now run your app using node server/server.js
and if everything goes well you would see your react application up and running on http://localhost:3000 like shown below
Ok so we finally have something meaningful to see when our server starts but the problem of volatile changes still exists. Well, Now that’s because we have not yet updated our frontend, to make it interact with fastify server and take benefit of the database. In order to achieve that we just need to modify three files namely actions.js
, reducer.js
and menu.js
. So let’s start by our actions file.
Step 2: Modify src/redux/actions/actions.js
In Here we’d be modifying readItems
action creator to make it capable of fetching data from the database. Currently this action creator does nothing special than returning an object with type : READ
we would be modifying it to return a function instead which would in return also dispatch some action creators but conditionally. But we know that normally an action creator can only return objects, So in order to override this traditional behaviour we would be using redux-thunk
module. You have installed it already in Part 1 so no need to install it again. Until now it’s need didn’t arise so we didn’t use it but we will now. In case you never heard this module before, you can know about redux-thunk in great detail here. But in a nutshell it’s just a module which let’s us write action creators which can return functions (which in turn also dispatches actions creators but conditionally) instead of an conventional action. As we discussed few lines above the returned function would also be dispatching some action creators So, let us first see what these extra action creators actually are -
export const FETCH_ITEMS_BEGIN = "begin fetching items";
export const FETCH_ITEMS_SUCCESS = "Items fetched successfully";
export const FETCH_ITEMS_FAILURE = "Failed to fetch items";export const fetchItemsBegin = () => ({
type: FETCH_ITEMS_BEGIN
})export const fetchItemsSuccess = items => ({
type: FETCH_ITEMS_SUCCESS,
payload: { items }
})export const fetchItemsFailure = errors => ({
type: FETCH_ITEMS_FAILURE,
payload: { errors }
})
So what do we have here, we have three new action creators let’s study their functionality one by one.
fetchItemsBegin
dispatches when we initiate fetching/loading data from database.fetchItemsSucces
dispatches when we have successfully fetched the data. And it is dispatched with apayload
property which contains the items that were fetched.fetchItemsFailure
dispatches when any sort of failure occurs while fetching data from database. It also has apayload
property which contains the errors.
These action creators have no importance on there own. They are not used independently rather they all are wrapped inside a function which dispatches them conditionally so let us discuss this function.
export const readItems = () => {
return (dispatch) => { // function starts
dispatch(fetchItemsBegin()); // fetching begins
return axios.get('/api/menuItems') // req data from server
.then(({data}) => { // if data is found
dispatch(fetchItemsSuccess(data)); // success
})
.catch(error => dispatch(fetchItemsFailure(error))); //errors
}
}
So here is our modified readItems
action creator, which returns function as opposed to action object. The function in turn dispatches fetchItemsBegin
in order to indicating that fetching has started, then we request data from server and depending on result dispatch either fetchItemsSuccess
or fetchItemsFailure
. If data is fetched successfully fetchItemsSuccess
is dispatched (with fetched data as argument) or in case some failure occurs fetchItemsFailure
is dispatched (with failures that occurred as argument). So that pretty much sums up what our modified readItems
action creator does and looks like. You may find the complete actions.js
here.
Now let’s modify reducer to handle the new action creators we just created.
Step 3: Modify src/redux/reducers/reducer.js
Let’s first import new action types which we need to handle.
import { FETCH_ITEMS_BEGIN, FETCH_ITEMS_SUCCESS, FETCH_ITEMS_FAILURE } from '../actions/actions';
Now how are we gonna handle them ? Okay so I’ll explain point wise how every action is going to be handled and then we’ll see all that in code. So here we go.
FETCH_ITEMS_BEGIN
: nothing would change exceptloading
would be set totrue
.FETCH_ITEMS_SUCCESS
:loading
would be set tofalse
andmenuItems
array would get filled with data received from server.FETCH_ITEMS_FAILURE
:loading
would be set tofalse
,menuItems
array would be truncated anderrors
would be set to errors found.
Now let’s see how we implement all this via code.
case FETCH_ITEMS_BEGIN: return {
...state,
loading: true,
errors: null
}case FETCH_ITEMS_SUCCESS: return {
...state,
loading: false,
menuItems: action.payload.items
}FETCH_ITEMS_FAILURE: return {
...state,
loading: false,
errors: action.payload.errors,
menuItems : []
}
Okay so by now, you must have understood how the the newly created actions are handled by the reducer both conceptually and by means of code. You may find the final version of reducer here.
Now we are only left with triggering of appropriate server endpoints by making appropriate HTTP requests from client side when we do one of the following Create data, Fetch data, Update data, Delete data. This can be accomplished with the help of a HTTP Client. So let’s get this done next up.
Step 4: Modify src/menu.js
HTTP client that we’ll use in this tutorial is axios
(feel free to use fetch
or any other HTTP client that you like, it hardly makes any difference). Now let’s import it at the top.
import axios from 'axios';
In order to read data from database, we just need to call the readItems
action creator and nothing more. i.e. we don’t need to make any HTTP request,readItems
does all by itself internally. We need to call readItems
as soon as app loads or reloads, or in other words when component mounts so that our redux store get’s filled with data automatically as soon as app loads or reloads. Therefore we’ve placed it in componentDidMount
life cycle method . Code for this is shown below.
componentDidMount() {
this.props.readItems();
}
In order to add data, we need to make HTTP POST
request at /api/menuItems
with data of new Item to be created and it sends response in form of added Item details. It is placed in handleAddItem
handler, so that Item gets added to database as soon as person clicks Add button, let’s see code for this.
handleAddItem = ({ name, price }) => {
...
...
axios.post('/api/menuItems',{...newItem}).then(({data:{name}})=>{
console.log(`Item - ${name} added successfully`);
}).catch(e => console.log("Addition failed , Error ", e));
...
...
}
In order to modify a particular item having id
equals to id
,we need to make a HTTP PUT
request at /api/menuItems/:id
with the data needs to be corrected. In response server sends the copy of updated item. It is placed in handleUpdateItem
handler, Code for this is shown below.
handleUpdateItem = (item) => {
...
...
axios.put(`/api/menuItems/${item.id}`,{item}).then(({data})=>{
console.log(`Item - ${data.name} updated successfully`);
}).catch(e => console.log('Updation failed, Error ',e));
...
...
}
In order to delete a particular item having id
equals to id
. We just need to make a HTTP DELETE
request at /api/menuItems/:id
. In response it sends data of deleted Item back. It is placed in handleDeleteItem
. Code is shown below.
handleDeleteItem = (id) => {
...
...
axios.delete(`/api/menuItems/${id}`).then(({data:{name}}) => {
console.log(`Item - ${name} deleted successfully`);
}).catch(e => console.log("Deletion failed, Error ",e));
...
...
}
That’s all that for our menu.js
. You may find the final code of menu.js
here.
We are pretty much done with interconnecting our front to backend. And if you run your application now you’ll see that data is persistent and it stays even after application is reloaded. Don’t forget to build you react app before serving it. You may use parcel build src/index.html
to build your react app.
In case you experience some errors please refer to final code of this app present on GITHUB.
Okay, so now we are only left with Deployment process. If you want see how you can deploy any application having stack similar to this ,then please follow along.
Let’s make it Live
I’ll be using Heroku to deploy our application. In order to deploy any node app to heroku please follow these steps shown below.
- In your fastify/node server, ensure that your port is declared like this
port = process.env.PORT || 3000
. If you are not usingprocess.env.PORT
then your application is likely not detect dynamic ports and would crash very often in dyanmic environments like heroku. - Ensure that Git is added to the project. If not just execute
git init
and create a.gitignore
file as well to ignore the data we don’t want to add to git. which in this case are/node_modules
,package-lock.json
,.cache
- Now in order to stage all the files run
git add .
and in order to commit rungit commit -m "any message"
. - I expect you to have heroku cli, if you don’t then please insall it before proceeding. After you have installed heroku cli execute the commands shown below.
heroku create <Application_name>
git push heroku master
heroku open
Note : In case of some error, you can run heroku logs --tails
. To what went wrong.
Now if all things went good, within few minutes you will be able to see your application live on web.
So that’all folks, I hope that you liked the two part tutorial series in which we built a Fullstack Fastify React Application completely from scratch, Through out this tutorial we covered various concepts like Optimistic updating, React Redux concepts, etc. I hope that you learnt some new things from the tutorial and I wish you luck for the future
If you liked this post, I’d be very grateful if you’d help it spread by sharing it to a friend, via Twitter or Facebook. And as always please let me know in comments how did you find this tutorial. Thank you!