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.
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.
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
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.
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.
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.
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
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.
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)
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)
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.
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.
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
.
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.
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.
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.
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.