Skip to content

Instantly share code, notes, and snippets.

@amandeepmittal
Created November 4, 2018 16:07
Show Gist options
  • Save amandeepmittal/8c41a723fbea09ddc61158bd76f7907f to your computer and use it in GitHub Desktop.
Save amandeepmittal/8c41a723fbea09ddc61158bd76f7907f to your computer and use it in GitHub Desktop.

Building a MERN Stack app using Material UI

Subtitle: In this tutorial, you will learn how to create an authenticated fullstack application using MERN stack and styled it with Material UI components.

It gets overwhelming when you are trying to build a full-stack web application using a tech stack like MERN. Building a web application does not end having setting up a considerable back-end and connecting it with a client-side library like React to fetch and display data. This data is what the user will interact with. You need to focus on having substantial User Interface (otherwise known as UI) for your web application. This is where it gets overwhelming.

MERN stack is full-stack because it consists of MongDB, Express, React and Nodejs. Each of them is replaceable but it is a common practice to use them together. React is the library using which you build the front-end of the web application. Express is a Nodejs framework that helps you to build a server that communicates to and fro with a NoSQL database like MongoDB.

In this tutorial, I am going to walk you step by step by building a small web application using this technology stack. Along with that, you will learn how to use Material UI library such that our application looks good and you use the concepts learned here for your own applications.

Pre-requisites

Before we get started I need you to install all the tools we are going to need to set up our application.

  • Nodejs
  • MongoDB
  • yarn
  • create-react-app

The last in the list are installed using npm.

Set up the MERN App

To get started, you need to follow along below steps by opening your terminal and typing these commands. Do not worry, I leave a comment before each command using #.

https://gist.github.com/a347c46bc0e447eb9cf3ad2cc5a48a33

After this step, make sure your root project looks like below with some extra files and folders.

(ss1)

Inside the server directory we will keep all files related to server and only in index.js we are going to bootstrap the server. Let's start with one. To setup and learn what Babel is, please read here.

Next step is to define the configuration you will need to proceed with server creation. Inside server create a new file config/index.js and define the following inside it.

https://gist.github.com/28cfeb77f55cb7d5f3c21b852abd009e

For MongoDB, I am going to use a local instance. If you want to use a cloud service (free tier), please read the steps to set it up and consume in a Node server app here. However, do make sure add the dev script inside package.json.

https://gist.github.com/50396a32f1f6ea59d2332058a486e379

Connect Database and the Server

Inside config directory, create a new file called dbConnection.js. Let us start by defining the MongoDB connection.

https://gist.github.com/ca7ee2866245cc98d1407b5d5a3a2563

I am going to use Mongoose as ODM (Object Document Mapper) that helps to write queries inside the Node server and create business logic behind it. It also provides a schema based solution to create data models and define them in our Node app. Although MongoDB is a schema-less database, Mongoose helps in this area to understand the data structure and organize it at the same time. The least it can do is to make a connection between the Express app when it bootstraps and MongoDB instance on our local machine.

Let us create a small server in the index.js file of the root of our web app and see this in action.

https://gist.github.com/f6d5a02c1103ae96e92a99840fc54692

If you are getting a message like below (ignore the mongoose warning) that means our server is up and running and successfully connected to the local instance of the database.

Building The User Model

To demonstrate, I am going to create a user data model with properties to save the user data when a new user registers with our application. We are going to save user credentials and validate it using mongoose in this section. Create a new file inside server/models/user.js.

We start by importing necessary dependencies at the top of our file and then create a new Mongoose Schema, userSchema which is an object with properties. Typically, NoSQL databases are super flexible, in that they allow us to put whatever we want in them without enforcing any specific kind of structure. However, Mongoose adds a layer of structure on top of the typical MongoDB way of doing things. This helps us perform additional validation to ensure that our users are not submitting any random data into our database without us having to write tons of boilerplate code ourselves.

https://gist.github.com/de644f77a3ac61b638be82d5132300f9

Then we use the userSchema object to add a virtualpassword field. Note that, whatever property described inside the userSchema object is going to be saved in the MongoDB document. We are not saving the password directly. We are creating a virtual field first to generate an encrypted hash of the password and then save it in our database.

A virtual field is a document property that can be used to combine different fields or decompose a single value into multiple values for storage but never gets carried on inside the MongoDB database itself.

Using Nodejs crypto module we are creating a hash that updates the virtual password. The salt field is a randomly generated string for each password. This terminology is related to cryptography. We are also putting the logic of validating the password field and checking whether it is 6 characters long. Lastly, we export the User model to be used with routes and controllers logic in our server.

User Routes

Now, let us write the business logic first behind the routes to create for the React end to interact with the server. Create a new file server/controllers/user.js and write the following code. Import the user model first that from the previous section.

https://gist.github.com/9a3f9c216b7ef72b64f38f80bc00505a

I have also added a helper function inside a separate file at the location server/helpers/dbErrorHandler.js to gracefully handle any error that occurs in any of the routes like we are using in above and respond back with a meaningful message. I am not going to explain the logic of that. They pretty much JavaScript functions and not a new concept. You can download the file from here.

In the file above, we are creating three controller functions. The first one registerUser creates a new user in the database from the JSON object received in a POST request from the client. The JSON object is received inside req.body that contains the user credentials we need to store in the database. Further, user.save, saves the new user in the database. Do notice that, we are not creating a unique field which is common in this type of scenarios to identify each new user saved in our database. This is because MongoDB database creates a _id field each time a new record is saved.

The next function we are exporting is findUserById. It queries the database to find the specific details related to the user whose _id is provided in parametric route (which I will define shortly). If a matching user is found with that _id in the database, then the user object is returned and appended inside the req.profile.

findUserProfile controller function retrieves the user detail from req.profile and removes any sensitive information such as password's hash and salt values before sending this user object to the client. The last function deleteUser removes the the user details from the database.

Now let use the controller logic and add it to corresponding routes inside server/routes/user.js.

https://gist.github.com/180cdb88f6273de2f7fb14e082a496d7

The controller functions are first imported and then used with their corresponding route.

Auth Routes

To restrict access to user operations such as user logged in can only access their profile and no one else, we are going to implement a JWT authentication to protect the routes. The two routes required to sign in and sign out the user from our application are going to be inside a separate file server/routes/auth.js.

https://gist.github.com/6122d8ace3916c1a40af7f48a4a2ecf1

The first route uses an HTTP POST request to authenticate a user with email and password credentials. The second route is used when the user hits the signout button (which we will implement in our front-end). The logic behind how these two routes work has to be defined in another file. Create a new file server/controllers/auth.js with the following code.

https://gist.github.com/6d8dcc23066b61c3f1f455075546b886

I am using two JWT related packages from npm to enable authentication and protect our routes: express-jwt and jsonwebtoken. You have already installed them when we bootstrapped this project. The first controller function signin we are exporting receives user's credentials in req.body. Email is used to retrieve the matching user from the database. Remember, we have added a unique field when defining the userSchema.

https://gist.github.com/fa46301e5acdd74a8fa029a53b31c06a

Since we are also receiving user's password, we are going to verify it with the hash and the salt value that we have stored in our database. The signed JWT is returned to the client to authenticate the user with their details if successful. We are using browser's cookies here to store the JWT token. You can use the browser's local storage for this purpose.

The signout function above clears the cookie containing the signed JWT token. The last two functions are important for our application. Both requireSignin and hasAuthorization are used to protect access to certain routes from an unauthorized user. They check and validate the user on client whether they are authenticated to give access.

requireSignin method here verifies a valid JWT in the Authorization header of the request. hasAuthorization allows a user to operate protected routes by checking that the user who is sending the request is identical to the authenticated user. In our application we are going to use this on one protected route. We are going to delete the user profile and their data from the database in that route.

Now let us use these methods to protect user routes. Open server/routes/user.js.

https://gist.github.com/7d3ac7c7bb73ce5a2fae8e2a7aa0f16f

Finishing the back-end

With the routing logic set up, we can now complete the server by adding our routes to index.js file.

https://gist.github.com/d3879063a45eefd1974d772553a27365

To test these routes, open up a REST Client like POSTMAN or Insomnia and the URL http://localhost:4000/api/users with required fields in order to create a user.

(ss3)

If there are no errors, you are going to receive the message Successfully signed up!. This means the user has been added to the database. If you try to make a new user with same credentials, it will throw an error this time.

(ss4)

If you use a MongoDB Client to view the records of your local database like Mongo Compass or Robomongo, you can easily see newly created user's details.

(ss5)

Using the same user credentials, we are will attempt a signin. It should give us a JWT back.

(ss6)

It works. Except for the sensitive information that we eliminated from the route, we are receiving the token and a user object. Now let us find the user profile. Hit the URL http://localhost:4000/api/users/{USER_ID} where USER_ID is the same created by MongoDB database when adding the user record.

(ss7)

You have to add the Bearer before signed JWT returned from the previous request at the Header Authorization. This completes our API testing and now we can focus on building the front-end of our application.

Adding Material UI in React

There are a series of steps to follow to add the Material UI Library to our react app. Traverse in the client directory and follow the below steps. Since we are also going to use Icons in SVG form. So let us add that package too.

https://gist.github.com/74f7250229a0af9c7d472000a517f603

Material-UI uses Roboto font and we have to add it through Google Font CDN to our client side. Open public/index.html add the following. Let us also change the title.

https://gist.github.com/8a757ca3a0b46840266f75566bafff66

To see if everything installed correctly and is working, run the client project using command yarn start. This will open the default React app that comes with create-react-app at URL http://localhost:3000. To see our our assets (such as Roboto font) being loaded, go to Developer Tools and open Network tab. Refresh the page to reload the assets and you will notice that the font family is being loaded.

(ss8)

Defining the Home Page

Now let us build the first component of our application. Create a new file inside src/components/Home.js and put the following content.

https://gist.github.com/8b14bbb5960b8aab1f43c1472de783f4

The first component we are importing from @material-ui in this file is withStyles. It allows us to style a component by declaring a styles object with access top-level styles such as we are using theme with our home component. We will define these top-level theme related styles shortly in App.js. Next, we are importing Card, CardContent, CardMedia to create a card view. CardMedia is used to display any media file whereas CardContent is used with Typography to output text. Typography is used to present hierarchy based styles over text to the content as clearly and efficiently as possible.

Now open up App.js and add the following content.

https://gist.github.com/6418c7a961919be879dfd433a0e52f66

MuiThemeProvider and createMuiTheme classes are used to create default theme. The theme specifies the color of the components, darkness of the surfaces, level of shadow, appropriate opacity of ink elements, and so on. If you wish to customize the theme, you need to use the MuiThemeProvider component in order to inject a theme into your application. To configure a theme of your own, createMuiTheme is used. You can also make the theme dark by setting type to dark like we have done above. Lastly, <MuiThemeProvider theme={theme}> is where the top level styles are being passed to child components, in our case Home.

If you render the app by running yarn start you will get the below output.

(ss9)

Adding React Router

We need someway to navigate to different routes for the user to sign in and sign out. In this section, let us add react-router library to our app to solve our purpose.

https://gist.github.com/8ea58216e5b99c7850090bb1541d0d4a

react-router library is a collection of navigational components. To get started, create a new file inside src folder called Routes.js.

https://gist.github.com/c35da13f254953bc62bca3036c0a7666

The Route component is the main building block of React Router. Anywhere that you want to only render content based on the location’s pathname, you should use a Route element. Switch is used to group different Route components. The route for the homepage, our Home component does include an exact prop. This is used to state that route should only match when the pathname matches the route’s path exactly. To use the newly created Routes, we have to make some changes to App.js to make it work.

https://gist.github.com/ffeb84a5e75b0985717ce84f7376cbb1

The BrowserRouter defined above is used when you have a server that will handle dynamic requests.

Connecting Node server and React

I have written an article in detail explaing how to connect a Nodejs server with the React front end application here. I am not going undergo the whole process. Just open your package.json and the following.

https://gist.github.com/93de2d35ec5a2a78a7fb1f97be8863fa

Next, I am going to add methods to be used in different components that will handle API calls from our server side code. Create two new files inside utils directory: api-auth.js and api-user.js.

https://gist.github.com/36f0e84cd89d534b3956fa1e35f51a4e

In api-auth.js, add the following.

https://gist.github.com/9b61c7d78a46bec8374453dcefbe00e4

The signin method takes care of user credentials from the view component (which we will create shortly), then use fetch to make a POST call to verify the user credentials with the backend. The signout method uses fetch to make a GET call to the signout API endpoint on the back-end.

Auth Components

Next, let us setup all the necessary components for authentication and user profile such that we get to see them in action all at once. One by one I am going to create new files so please follow closely. Create a new directory inside components and call it auth. Then, create a new file auth-helper.js.

https://gist.github.com/3f10c00fadc3465ab78eec8bec740c64

These functions will help us manage the state of authentication in the browser. Using these methods our client side app will be able to check whether the user has logged in or not. To protect our routes such as user's profile from unauthorized access, we define a new component inside PrivateRoute.js and make use of the methods above.

https://gist.github.com/e8f59e108a303a1bb8c373fce2dbbdcb

We are going to use this component as an auth flow in the Routes.js we have defined. Components that rendered via this route component will only load when the user is authenticated. Our last component related to user authentication is to be defined inside Signin.js.

https://gist.github.com/86575a1c939bfe605eb7c92666588c2e

This is a form component that contains email and password field (_as we defined in state above) for the user to enter to get authenticated. redirectToReferrer property in state is what we are using if the user gets verified by the server or not. If the credentials entered by the user are valid, this property will trigger Redirect component of react-router-dom.

User Components

Similarly to our auth routes, we are going to separate our user components inside components/user/ folder. First, we need a React component to register a new user. Create a file called Signup.js.

https://gist.github.com/70e788e929beda6a86d8511625f26b9b

We start the component by declaring an empty state that continas various properties such as name, email, password and error. The open property is used to capture the state of a Dialog box. In Material UI, a Dialog is a type of modal window that appears in front of app content to provide critical information or ask for a decision. The modal in our case will either render an error message or the confirmation message dependening on the status returned from the server.

We are also defining two handler functions. handleChange changes the new value of every input field entered. clickSubmit invokes when a user after enterting their credentials, submit the registeration form. This function further calls registerUser from the API to send the data to the backend for further actions.

Create a new file called Profile.js.

https://gist.github.com/98707dd3dadeece9ac1794d7ed12c234

This component show's a single user who is authenticated by the back-end of our application. The profile information of each user is stored in the database. This is done by the init function we have defined above the render function of our component. We are using redirectToSignin redirect to the user on signout. We are also adding a delete profile button as a separate component which has to be defined in a separate file called DeleteUser.js.

https://gist.github.com/b9581ee2b832c28b136413d31ba2968c

This component is used for deleting the user profile that exists in the database. It uses the same deleteUser API endpoint we defined in our back-end. deleteAccount method is reponsible for handling this task.

Completing the Navbar

In this section we are going to complete our client side routes by leveraging a Navbar component. Create a new file component/Navbar.js.

https://gist.github.com/083feeb026f36b294f73514641c3ac98

This Navbar component will allow us to access routes as views on the front-end. From react-router we are importing a High Order Component called withRouter to get access to history object's properties and consume our front-end routes dynamically. Using Link from react-router and auth.isAuthenticated() from our authenticatin flow, we are checking for whether the user has access to authenticated routes or not, that is, if they are logged in to our application or not. isActive highlights the view to which the current route is activated by the navigation component.

Running the Application

Next step is to import this navigation component inside Routes.js and define other necessary routes we need in our app. Open Routes.js and add the following.

https://gist.github.com/9a7dd92a9379050ebd6084258c47f2cb

After completing this test, let us test our application. Make sure you are running the backend server using nr dev command in one tab from your terminal and using another tab or window, traverse to client and run the command yarn start. Once the application starts, you will be welcomed by the Home page as below.

(ss10)

Notice in the navbar there are three buttons. The home icon is for Home page highlighted red in color. If you move on to the sign in page, you will se the sign in button highlighted. We already have one user registered to our application (when we were building the API). Please enter the credentials (email: jane@doe.com and password: pass1234) as shown below and submit the form.

(ss11)

On submitting the form you will be redirected to the home page as per the component logic. The changes can be noticed at the navigation menu. Instead of sign-up and sign-in, you will see My Profile and Sign Out button. Click My Profile and you can see the current user's details.

(ss12)

On clicking the delete icon it will delete the user. You can also try logging out of the application by clicking on the sign out button from navigation and then, you will be redirected to the home page.

Conclusion

We have reached the end. Even though this tutorial is lengthy and a lot is going on, I am sure if you take your time, you will understand the concepts and the logic behind it. We have now successfully built a full-stack MERN application that uses JSON Web Tokens as authentication strategy. If you want to learn how to deploy this application, you can continue to read this article.

You can find the complete code for this tutorial below in a Github repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment