Building Distributed Systems Application— ReactJS, NodeJS, and MongoDB (Part 1)

Rishabh Jain
Dev Genius
Published in
17 min readApr 19, 2022

--

As we are aware the world is rapidly moving from monolithic architecture to microservices architecture when it comes to application deployment. But what and how is a microservices architecture developed and deployed. The how part is what I will be discussing here.

Recently, I was introduced to microservices architecture and concepts. Reading about the advantages and why part made me curious about it. Hence, decided to come up with this article to learn and understand the details more thoroughly and share it with others. The GitHub repository of the project built in this post is present here.

In this article, we will first develop microservices then make them communicate with each other using HTTP calls and store the required data in the persistent storage. Below is the architecture diagram of the application.

Fig. Architecture diagram

Technologies used to build the application
1. Web Application - ReactJS
2. Back End Server - NodeJS
3. Database - MongoDB

Tools version at the time of implementation
1. npm- 8.1.2
2. NodeJS- 16.13.2
3. ExpressJS- 4.17.3
4. MongoDB- 5.0.6
5. ReactJS- 17.0.2

Running the application on M1- macOS Monterey 12.2.1.

Not getting much into the details of the tools let’s begin with the implementation. First, let us set up the ReactJS and NodeJS project. In the react project we will use a functional component rather than a class component (Functional vs Class Component).

Initial setup of ReactJS project

The following command will create the skeleton for our application

npx create-react-app web_app_front_end

To run the starter code execute the following

cd web_app_front_end/src
npm start
Fig. ReactJS application running

Initial setup of NodeJS project (ExpressJS)

Let’s create the skeleton of the backend using the ExpressJS (framework built on NodeJS) by creating a server with an endpoint that serves us Hello World!

  1. Create a folder called server_back_end in our root directory
  2. Create an apps.js file inside the above folder with the code mentioned on this link
    In this article, we will be running the NodeJS server on port 3001 because the ReatJS project is running on port 3000

— First of all, do the right setup before we run the NodeJS server

cd server_back_end
npm init //To initialise npm
npm install express --save

— To run the NodeJS server

node app.js

My recommendation to run the node server is to use the nodemon package because it restarts the server whenever there are changes in any of the project files. To install and run the application using nodemon

npm install -g nodemon         //To install globally
npm install nodemon --save //To install only for this project
nodemon app.js

Test the server by browsing http://localhost:3001/ on the web browser

Fig. NodeJS server running

Setup OAuth 2.0 Client

To integrate Google OAuth, we need to generate an OAuth client ID on the Google developer console. We will need the OAuth client ID in Task1 implementation.
1. Create a new project on google console
Name the project of your choice
2. Select the project you created from the dropdown on the header bar
3. Go to the Credentials page from Apis & Services section from the left tab
4. Select and create a new OAuth client ID under Create Credentials button (You might be asked to Configure Consent Screen before creating a new ID, continue from step no. 5 after completing step no. 8)
5. Select Web application from the application type dropdown
6. Add the following URI in Authorized JavaScript origins (If we don’t add this the Google OAuth won’t work in our web application)
- http://localhost
-
http://localhost:3000
NOTE: You might need to add more URIs when the application is deployed on any other server
7. Click on create and it generates a Client ID and Client Secret (Copy Client ID we will use it in our react application)
8. On the OAuth consent screen select ‘External’ user type and in the next section fill the required details (OPTIONAL)

Implementation

We will divide the tutorial into multiple tasks which are as follows-

Task 1 - Add Google OAuth Button on the front end(client) (Before adding the button we need to set up the OAuth client on the google developer console)
Task 2 -
1. On successful authentication send the user details to the server by making an API call
2. Navigate from the main component to the Dashboard component with the user data
Task 3 - Receive data on the server by creating an API endpoint
Task 4 - Store the data in MongoDB and send the callback response to the web app
Task 5 - Parse the callback response on the client-side and redirect the user to the dashboard

Task 1

To include Google OAuth in our application we will be using the react-google-login package.

Install the package using

npm i react-google-login --save

--save option is useful because it will add packages only for this project

Let us use the skeleton code and create a login page for the user.
We will be using functional components rather than class components as mentioned in the beginning.
1. We will create a new component called Login.js and export it for consumption in App.js

I. In Login.js we will add the following

  • GoogleLogin tag inside the return method for the sign-in button
    * Create a .env file in the root folder of the application and place your Client ID in a variable named REACT_APP_CLIENT_ID
<GoogleLogin
clientId={process.env.REACT_APP_CLIENT_ID} //Client ID generated from google cloud developer console. Retrieving from .env file
buttonText=”Login”
onSuccess={onSuccess} //Success callback function
onFailure={onFailure} //Failure callback function
cookiePolicy={‘single_host_origin’}
isSignedIn={true} //To keep user logged in
/>
  • Callback methods onSuccess and onFailure to check results of Google OAuth response
    1. When the authentication is successful onSuccess method is called, in this case, we will have to do 3 subtasks (Implementation is done in Task 2 later in this post)
    a. make an API call to the server to save user details to the database
    b. Update the API response on the webpage to indicate to the user the status using react state hook
    c. Navigate to the Dashboard component along with the user details
    2. When the authentication is failed the onFailure method is called, in this case, we will have to do only 1 task
    a. Inform the user that Google OAuth has failed using react state hook

II. In App.js we will add the following things

a. import Login from ‘./components/Login’;
b. Place <Login/> inside return method just like a HTML tag

NOTE: return method is the entry point of the component and things inside it are rendered on the webpage

Before we proceed further we can test if the Google OAuth is working or not using console.log() with respective messages.
All the user Google account details are present in profileObj of the response JSON.

Fig. 1.1 Google OAuth successful

Task 2

This task is divided into 2 sub-tasks
Step 1. Send user details to the backend server (Make an API call)
Step 2. Navigate to the Dashboard component with the user data

The flow will be making an API call to the server, on the successful response from the server we will navigate to the Dashboard component.
However, the navigation part along with data passing has many details. Hence, we will first complete step 2 and then integrate step 1 with it.

Step 2:
To navigate between components we need to do additional configurations in the code. First, we will map each component to a specific URL path.
For example, if we open http://localhost:3000/ or http://localhost:3000/Login in the browser, it should render the login page component. We can map any path to any component.
To map each component we use the Routes tag of react-router-dom this helps us to map a path to the component.

Before, proceeding further install react-router-dom using the following command

npm install react-router-dom --save
  • We have to do a couple of addition and changes to the code.
  1. Create a new file that will hold routing details. Name the file as routes.js
    Note: It won’t be a component it will be more of a config file that will hold a mapping of path and components.
    The syntax to define the mapping is Routes tag as a parent tag with Route as the child tag with mandatory attributes
    <Routes>
    <Route path=”/” element={<App />} />
    <Route path=”/Login” element={<App />} />
    </Routes>

    Definition of Routes - A container for a nested tree of elements that renders the branch that best matches the current location.
    Route - Declares an element that should be rendered at a certain URL path.
    Attributes:
    path - URL path
    element - component to load when the above URL is loaded

Definition of Routes - A container for a nested tree of elements that renders the branch that best matches the current location.
Route - Declares an element that should be rendered at a certain URL path.
Attributes:
path - URL path
element - component to load when the above URL is loaded

  1. Modification required in the index.js file
    - Import the newly created file routes.js file as RouterDetails
    - Replace <App/> tag with
    <BrowserRouter>
    <RouterDetails/>
    </BrowserRouter>
    Here BrowserRouter helps to keep your UI in sync with the URL. Whenever the URL changes this is responsible to find and load a component matching the URL from our predefined paths mentioned in routes.js
  • Here, we are now ready to do the navigation to the new component when the user successfully logs in using Google OAuth.
    For navigation, we need to
    1. create a new component(file) let us call it Dashboard.js , and a ‘Welcome to Dashboard’ text or any other text for testing purposes.
    2. Add path and component mapping in the routes.js file
    <Route path=”/Dashboard” element={<Dashboard />} />
    3. To load the Dashboard component from the Login component we have to assign the task to someone to load and render it on the webpage.
    This is handled using the useNavigate() hook provided by the
    react-router-dom package to let us navigate programmatically.
    Modifications in Login.js file:
    — Create a local variable and store the reference of the function in a variable
    const navigateObj = useNavigate();
    —Inside onSuccess callback
    navigateObj(‘/Dashboard’) //Dashboard Name of the component.
    The name should be the same as the one mentioned in the routes.js file
    4. Let us test it before proceeding further. Run npm start command and do the google sign-in, once successful we will be able to see the following web page (Refer to Fig 2.2.1). (I have applied some CSS but you will be able to see the ‘Welcome to Dashboard’ text)
    5. Moving forward, let's add a real-world scenario in the navigation part.
    We need to pass the email id of the authenticated user from the login page to the dashboard page and display it on the webpage.
    The text displayed will ‘Welcome to Dashboard - EMAIL_ID’
    - We will add certain inbuilt properties provided by the NagivateFunction, we add it inside the onSuccess callback function in the Login.js file.
    navigateObj(‘/Dashboard’,{ state: { userEmail: res.profileObj.email}, replace: true })
    A detailed explanation of the new properties.
    state - a JSON object to pass user-defined keys with the desired value
    replace - true (Default - false), will replace the existing component with the new component which means if the back button of the browser is clicked it won’t redirect to the previous page. In our case, the Login component will be replaced with the Dashboard component and we won’t be able to go to the Login page on the back button click of the browser.
    userEmail - Can be named anything it is user-defined. The value for it is the user email id which is present in the response object of the onSuccess method under profileObj i.e. res.profileObj.email
    6. Now that we have passed the values from Login component, let's have a look at how we can receive the value in Dashboard component and display it on the web page.
    - As we used the NavigateFunction hook for navigation, we will Location hook from the same package to retrieve the user email value.
    - To do so add the below in Dashboard.js
    a. import { useLocation } from “react-router-dom”;
    b. Create two local variables one to get the state key and the other to retrieve the userEmail key (passed in navigateObj in step 5 above)
    const {state} = useLocation(); //state keyword can’t be changed this should be as mentioned
    const {userEmail} = state //userEmail is the key name we mentioned above. It has to be the same
    c. Place {userEmail} besides the welcome text inside the return method of the component.
    Welcome to Dashboard — {userEmail}
    {VARIABLE_NAME} displays the value present in the variable
    You will be able to see the user email when we run the react app again. (Refer to Fig 2.2.2)
Fig 2.2.1 Navigation without data
Fig 2.2.2 Navigation with data

Step 1:

As promised, we will now dig into the networking part where we are going to send user details to the backend application.
For this, we will consume the fetch() method which is powerful and straightforward to implement. Considering that moving forward we will add more functionality to the current application i.e. probably making more API calls. For this reason, would like to make a reusable method specifically to do the network calls. The method will be provided with 3 arguments - endpoint, method type(GET/POST), and request body in case of a POST request.

The method will process the parameters and create the final URL then call the right method with the body if provided. The fetch() method documentation can be found here.

For POST requests we need to add headers and body (if not empty) to the fetch method. The reusable code is placed in the new file Called ApiCall.js in the ./src/Utils directory. The method name is ApiCall and we have marked it as an async function i.e. it won’t run on the main thread rather in the background thread however we are going to wait until the response is received from the server using then() method because the response will be used to decide the next step.

A bit confusing right? Let’s see how the flow will be in our code.
From Login.js inside our onSuccess() method we will call the ApiCall() method with the required parameters. Which will then do the processing and retrieve the response from the server, then it will return the response to the calling function and we will be able to parse or access the data inside the then() method. If the response looks ok we will navigate to the dashboard else will display the error.
The onSuccess method will look like the below and we also need to import the ApiCall method.

const onSuccess = (res) => {var oUserDetails = {"name":res.profileObj.name, "email_id":res.profileObj.email, "profile_url":res.profileObj.imageUrl}ApiCall(process.env.REACT_APP_API_POST_NEW_USER_DETAILS, process.env.REACT_APP_API_METHOD_TYPE_POST,
JSON.stringify(oUserDetails)).then(
(response) => {
console.log(response)
}
)

};

In this post, just providing a gist of the async function. For more details check this out.

Task 3

Congratulations, you reached task 3, we have almost completed the web app part. Now, we will look into the working of the backend module according to the requests received from the web app.
FYI, we are using Express JS for the backend module and will be working with files located in the server_back_end directory.

Let’s get started with it. At the start of this tutorial, we had completed and ran the initial setup. As of now, from the web app, 1 API call is made to the backend i.e. after successful Google OAuth login.
We will store the user details in MongoDB using our backend module.

Begin by creating an endpoint for it. Add the following to the app.js. We are going to store the user's name, email id, and profile image URL.

app.post(‘/newUserDetails’, (req, res) => {
console.log(req.body) //To check the request body
let pName = req.body.name
let pEmailId = req.body.email_id
let pProfileUrl = req.body.profile_url
}

The web app will be sending a JSON object with all the above-mentioned values. Before proceeding to the storage part, we need to retrieve the values from the JSON object. The JSON can be retrieved from the request object i.e. using the req.body.KEY_NAME.
KEY_NAME
- as mentioned on the web app API call

Note: A. The JSON of the request can only be retrieved from the req.body by adding app.use(express.json()); at the top of our app.js file
Let’s dive deep into it and look at what exactly it does.

1. The app.use() function adds a new middleware to the app. Essentially, whenever a request hits the backend, Express will execute the functions we passed to app.use() in order.
2. express.json() is a built-in middleware function in Express that parses incoming JSON requests and puts the parsed data in the req.body. If we were using NodeJS then this parsing should have to be handled by the developer.

B. If we run our application at this stage we will see a CORS error in the console when making an API call from the frontend to the backend. To solve that we need to allow access from all the origins or we can even allow access to specific domains by adding the following code in the app.js file or you can try using the cors package(haven’t tried it).

app.use(function (req, res, next) {
res.setHeader(“Access-Control-Allow-Origin”, “*”);
res.setHeader(“Access-Control-Allow-Headers”, “*”);
next();
});

setHeader() - will allow access to all incoming requests
next() - is an inbuilt method that gives control to the developer to decide whether to pass control to the next function or not.

When we run both, the web app and back end and log in using Google Sign we will be able to see the JSON object in the terminal running the backend service(Refer to Fig 3.1).

Fig 3.1 User Details Received on the Backend

Task 4

The task is to store the data in MongoDB and send the callback response.
Now that we need to store the user details in MongoDB via NodeJS we will be consuming a package called mongoose(a wrapper around the mongo driver) and need to code multiple things in our backend application.

First, install the mongoose package using

cd server_back_end/
npm install mongoose --save

Following are the TODOs for this task
1. Make a connection with the database
2. Create Schema and Model of the Collection (Table in relational database i.e. MySQL)
3. Create a controller file that will do all the database operations on user details collection and send the response callback to the web app.

  1. First, we need to connect to the database whenever we start our node application.
    Create a new file called database.js with the function connectDB() which has the connection code.
mongoose.connect("mongodb://127.0.0.1:27017/distributed_systems_tutorial");
const conn = mongoose.connection;
conn.on("connected", function () {
console.log("database is connected successfully");
});

The above code will try to connect to the database named distributed_systems_tutorial located on the same machine. The status of the connection is saved in mongoose.connection. If it doesn’t connect then make sure your MongoDB daemon is running on the machine. For troubleshooting check this out. It will also create the database if it doesn’t exist.

Addition required in app.js file. Import the DB method and call it whenever the application is started.

const oDatabse = require("./database"); //Import
/**
* Listen method as the name sound listens to the connection requests made to above endpoints
*/
app.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
oDatabse.connectDB()
});

Therefore, when we start our backend service we will see the status of the DB connection in the terminal.

Fig 4.1 Database connection

2. Create schema and model to store user details in the database. Schema as the name sounds it’s the structure of the document, fields which could be present in a document of a collection. Whereas, models are used to query the document of the collection.
For a better understanding of both check this out.

Create a new file named userDetailsModel.js in the models folder that will contain our schema and model configuration. The file will look like

const mongoose = require("mongoose"); //Import//Define the structure of the document
const userDetailsSchema = new mongoose.Schema({
name: String,
email_id: String,
profile_url: String
});
//Name of the collection (table) in our database
const userDetailsCollectionName = "userdetails"
//Mongoose compiles a model of the given schema
const userDetailsModel = mongoose.model(userDetailsCollectionName, userDetailsSchema)
//Export it so that it can be consumed from another file of the project
module.exports = {
userDetailsModel
}

3. Finally, we will look into the database operations i.e. insert or update the user details. Create a new file and name it userDetailsController.js in the controllers folder. The main logic of this file will be to insert user details or update the details if already exists. To achieve this mongoose provides a method called findOneAndUpdate().
The parameters of the method are filter, update, options, and a callback function.
filter — We will be considering the user email id as a unique key in each document and use it to query the collection
update — will be the object to be inserted or updated
options — specific to MongoDB to decide what to do in certain cases when using the findOneAndUpdate method
callback — to decide what to do after the DB operation is completed. Here we are sending the response callback to the web app.

All the logic is written in dbInsertOrUpdateDetails() function of userDetailsController file.

//userDetailsController.js
const UserDetailsModel = require("../models/userDetailsModel"); //Import user details model
async function dbInsertOrUpdateDetails(req, res){const filter = {email_id: req.body.email_id}const options = {upsert: true, new: true} // When upsert is set to true it will insert the details if the document doesn't exists and if new is set to true itt will return the updated document in the query else the old documentUserDetailsModel.userDetailsModel.findOneAndUpdate(
filter, //Finds the document in the collection matching the values of filter
req.body, //Maps to the model we created in the userDetailsModel file
options,
(error, doc) => {
if(!error){
console.log(doc)
//Send response back to the user i.e to the web app
res.send({"status":"success", "msg":"Data inserted successfully"})
} else {
console.log(error)
//Send response back to the user i.e to the web app
res.send({"status":"error", "msg":"Something went wrong!"})
}
}
);
}
module.exports = {
dbInsertOrUpdateDetails
}

We export the above method and use it in the newUserDetails endpoint mentioned in the app.js file.

//app.js file
const UserDetailsController = require ("./controllers/userDetailsController") // Import the file
/**
* Endpoint to receive new user details
*/
app.post('/newUserDetails', (req, res) => {
console.log(req.body)
UserDetailsController.dbInsertOrUpdateDetails(req,res)
})

As we test the entire application the data should be present in MongoDB. The GUI tool MongoDB Compass can be used to easily view the data in the database. Use the URL mentioned in our backend code to connect to the database.

mongodb://127.0.0.1:27017
Fig 4.2 MongoDB Compass

Once connected, select the database and check the collection.

Fig 4.3 Data in the collection

Task 5

Parsing the callback response on the web app and redirecting the user to the dashboard.
This task is the smallest task of all. In the onSuccess method of the Login.js file in the web_app_front_end/src/ folder, we need to parse the JSON object and check the ‘status’ key. A simple if-else is what we need to complete this task. The onSuccess method will be as follows.

const onSuccess = (res) => {var oUserDetails = {"name":res.profileObj.name, "email_id":res.profileObj.email, "profile_url":res.profileObj.imageUrl}ApiCall(process.env.REACT_APP_API_POST_NEW_USER_DETAILS, process.env.REACT_APP_API_METHOD_TYPE_POST,
JSON.stringify(oUserDetails)).then(
(response) => {
console.log(response)
if(response["status"] === "success") {
setApiCallResponse("Login Successful! Redirecting...")
setTimeout(() => {
navigateObj('/Dashboard',{ state :{ userEmail: res.profileObj.email}, replace: true })
}, 200);
} else {
setApiCallResponse(response["msg"])
}

}
)
};

Now, you can run the entire application end to end as mentioned at the beginning of this post. Cheers!

Conclusion

In the microservice approach, each service could be deployed on a separate machine. However, I am running everything locally on the same machine. The post is a bit long as laying the foundations takes time. However, the next one will be comparatively small and more concept-oriented. The project GitHub repository is present here. Stay tuned for Part 2.
Any suggestions or improvements? please feel free to comment.
Thank you for reading!

--

--