Acing the MERN Stack by Building an Exercise Tracker

by

MERN stack is a JavaScript stack used for the fast and easy development of Single Page Applications (SPAs). It comprises four technologies – MongoDB, Express, React, and Node.js.

  1. MongoDB: It is a document-oriented, cross-platform database.
  2. Express: Express is a web-app framework for Node.js.
  3. React: React is a front-end library used to build user interfaces.
  4. Node.js: This is a run-time environment which executes the code outside of a browser.

MERN Stack Bootcamps will guide aspiring developers in developing, testing, deploying, and securing codes for fault-tolerant applications. Let us learn this technology by building an exercise tracker using MongoDB. However, you will have to create a MongoDB Atlas account and create a cluster to code. Once the cluster is created, choose the cloud provider and the region to store the data. Configure security using IP Whitelist addresses and the database user. (refer to images below)

Developing the Exercise Tracker

[Note: Type everything that follows ‘$’ symbol at the command prompt in a terminal window]

  1. First, verify you have Node.js installed in your system by using the following command:

$ node -v

  1. Then, create the React project using create-react-app using the following npx command:

$ npx create-react-app mern-exercise-tracker

  1. Switch to the newly created directory by typing:

$ cd mern-exercise-tracker

  1. Start the development server for the front-end of the app using:

$ npm start

Before moving further, create the back-end and connect it to the MongoDB Atlas using the following steps:

  1. Create a new folder inside the ‘mern-exercise-tracker’ (root folder) using the command:

$ mkdir backend

$ cd backend

  1. Then create a package.json file inside the folder by running the following command:

$ npm init -y

  1. Install dependencies using:

$ npm install express cors mongoose dotenv

  1. Then install one package globally:

$ npm install -g nodemon

Setting up the Backend Server

  1. Make a server.js file inside the backend directory. Inside this file, create an express server, attach the cors and express.json middleware, and then make the server listen on port 5000. Add the following code:

const express = require(‘express’);

const cors = require(‘cors’);

require(‘dotenv’).config();

const app = express();

const port = process.env.PORT || 5000;

app.use(cors());

app.use(express.json());

app.listen(port, () => {

console.log(`Server is running on port: ${port}`);

});

Call the server using the command:

$ nodemon server

  1. Now, connect the database to MongoDB Atlas by adding the following line to the server.js file after cont cors = requre(‘cors’);

const mongoose = require(‘mongoose’);

After the line app.use(express.json()); , add the following command:

const uri = process.env.ATLAS_URI;

mongoose.connect(uri, { useNewUrlParser: true, useCreateIndex: true

}

);

const connection = mongoose.connection;

connection.once(‘open’, () => {

console.log(“MongoDB database connection established successfully”);

})

  1. Now, for the connection to work, add the right ATLAS_URI environment variable. For this, create a file named .env . Follow the steps:

Go back to MongoDB Atlas dashboard > Connect to Cluster0> Connect Your Application using a connection method. (refer the images below)

  1. Type ‘ATLAS_URI=’ in the .env file and paste the string you copied.

ATLAS_URI=mongodb+srv://mean123:<password>@cluster0-91icu.gcp.mongodb.net/test?retryWrites=true

Replace the password you used to set up the user in the password field.

In the terminal, you will now be able to see ‘MongoDB database connection established successfully’, following which you need to restart the server.

Creating the Database Schema

Next, let us create the database schema using Mongoose with two entities: Exercises and Users.

  1. Create a new folder named ‘models’ inside the backend folder and then create two files named exercise.model.js and user.model.js. Now add the following code for the user model:

const mongoose = require(‘mongoose’);

const Schema = mongoose.Schema;

const userSchema = new Schema({

username: {

type: String,

required: true,

unique: true,

trim: true,

minlength: 3

},

}, {

timestamps: true,

});

const User = mongoose.model(‘User’, userSchema);

module.exports = User;

  1. The user schema contains only one field called ‘Username’, with validations. It is required, three characters long and white spaces trimmed off the end. Next, add the following code to the exercise model:

const mongoose = require(‘mongoose’);

const Schema = mongoose.Schema;

const exerciseSchema = new Schema({

username: { type: String, required: true },

description: { type: String, required: true },

duration: { type: Number, required: true },

date: { type: Date, required: true },

}, {

timestamps: true,

});

const Exercise = mongoose.model(‘Exercise’, exerciseSchema);

module.exports = Exercise;

The exercise schema has four fields without much validation.

Adding the Server API Endpoints

The next step is to add API endpoint routes, so that the server can perform CRUD tasks. For that,

  1. Create a folder named ‘routes’ in the backend folder. Inside ‘routes’ create two files – exercises.js and users.js.
  2. Next, you need to tell the server to use these files. For this, towards the end of the server.js, before the line:

app.listen(port, function() {

Add the following code:

const exercisesRouter = require(‘./routes/exercises’);

const usersRouter = require(‘./routes/users’);

app.use(‘/exercises’, exercisesRouter);

app.use(‘/users’, usersRouter);

  1. Next step is to build the router files. There are two endpoints: one that handles HTTP GET requests and the other one that handles HTTP Post requests. Add the following code to the users.js file;

const router = require(‘express’).Router();

let User = require(‘../models/user.model’);

router.route(‘/’).get((req, res) => {

User.find()

.then(users => res.json(users))

.catch(err => res.status(400).json(‘Error: ‘ + err));

});

router.route(‘/add’).post((req, res) => {

const username = req.body.username;

const newUser = new User({username});

newUser.save()

.then(() => res.json(‘User added!’))

.catch(err => res.status(400).json(‘Error: ‘ + err));

});

module.exports = router;

  1. Add the same endpoints to the exercises.js file as follows:

const router = require(‘express’).Router();

let Exercise = require(‘../models/exercise.model’);

router.route(‘/’).get((req, res) => {

Exercise.find()

.then(exercises => res.json(exercises))

.catch(err => res.status(400).json(‘Error: ‘ + err));

});

router.route(‘/add’).post((req, res) => {

const username = req.body.username;

const description = req.body.description;

const duration = Number(req.body.duration);

const date = Date.parse(req.body.date);

const newExercise = new Exercise({

username,

description,

duration,

date,

});

newExercise.save()

.then(() => res.json(‘Exercise added!’))

.catch(err => res.status(400).json(‘Error: ‘ + err));

});

module.exports = router;

5. Test the server API using tools like Insomnia or Postman. For this create a POST request using JSON.

6. Enter the URL ‘http://localhost:5000/users/add’ and then enter the JSON like below and click ‘Send’. The successful response will be‘User Added’.

{

“username”: “beau”

}

7. Following this, you can send a GET request to ‘https://localhost:5000/users/’ to get a list of users back. The user added by you now will be displayed on the MongoDB Atlas Dashboard too.

8. Next, you need to add exercises to ‘http://localhost:5000/exercises/add’ using the following:

{

“username”: “beau”,

“description”: “run”,

“duration”: 5,

“date”: “2019-04-29T21:19:15.187Z”

}

and

{

“username”: “beau”,

“description”: “bike ride”,

“duration”: 20,

“date”: “2019-04-30T21:19:15.187Z”

}

9. Once testing is done, add the following code after the routes in ‘js’.

router.route(‘/:id’).get((req, res) => {

Exercise.findById(req.params.id)

.then(exercise => res.json(exercise))

.catch(err => res.status(400).json(‘Error: ‘ + err));

});

router.route(‘/:id’).delete((req, res) => {

Exercise.findByIdAndDelete(req.params.id)

.then(() => res.json(‘Exercise deleted.’))

.catch(err => res.status(400).json(‘Error: ‘ + err));

});

router.route(‘/update/:id’).post((req, res) => {

Exercise.findById(req.params.id)

.then(exercise => {

exercise.username = req.body.username;

exercise.description = req.body.description;

exercise.duration = Number(req.body.duration);

exercise.date = Date.parse(req.body.date);

 

exercise.save()

.then(() => res.json(‘Exercise updated!’))

.catch(err => res.status(400).json(‘Error: ‘ + err));

})

.catch(err => res.status(400).json(‘Error: ‘ + err));

});

The GET endpoint returns an exercise item, and DELETE endpoint deletes an exercise. Next, test these endpoints using the tools.

Frontend

  1. Start the front end development using the following command:

$ npm start

  1. Add the Bootstrap CSS to the project folder by adding the command:

$ npm install bootstrap

  1. Ensure that the CSS file is imported into App.js by adding the line:

import “bootstrap/dist/css/bootstrap.min.css”;

  1. Then, embed the JSX code in the element – <Router></Router>. This is how App.js will look like:

import React from ‘react’;

import { BrowserRouter as Router, Route } from “react-router-dom”;

import “bootstrap/dist/css/bootstrap.min.css”;

function App() {

return (

<Router>

<div className=”container”>

Hello World

</div>

</Router>

);

}

export default App;

  1. Inside the <Router> add the configuration. Replace ‘Hello World’ with:

<Navbar />

<br/>

<Route path=”/” exact component={ExercisesList} />

<Route path=”/edit/:id” component={EditExercise} />

<Route path=”/create” component={CreateExercise} />

<Route path=”/user” component={CreateUser} />

  1. Import the files into App.js after importing for bootstrap.

import React from ‘react’;

import { BrowserRouter as Router, Route } from “react-router-dom”;

import “bootstrap/dist/css/bootstrap.min.css”;

import Navbar from “./components/navbar.component”

import ExercisesList from “./components/exercises-list.component”;

import EditExercise from “./components/edit-exercise.component”;

import CreateExercise from “./components/create-exercise.component”;

import CreateUser from “./components/create-user.component”;

 

function App() {

return (

<Router>

<div className=”container”>

<Navbar />

<br/>

<Route path=”/” exact component={ExercisesList} />

<Route path=”/edit/:id” component={EditExercise} />

<Route path=”/create” component={CreateExercise} />

<Route path=”/user” component={CreateUser} />

</div>

</Router>

);

}

 

export default App;

  1. Create a directory called ‘components’, and inside the directory create a file named ‘component.js’.

 

import React, { Component } from ‘react’;

import { Link } from ‘react-router-dom’;

 

export default class Navbar extends Component {

 

render() {

return (

<nav className=”navbar navbar-dark bg-dark navbar-expand-lg”>

<Link to=”/” className=”navbar-brand”>ExcerTracker</Link>

<div className=”collpase navbar-collapse”>

<ul className=”navbar-nav mr-auto”>

<li className=”navbar-item”>

<Link to=”/” className=”nav-link”>Exercises</Link>

</li>

<li className=”navbar-item”>

<Link to=”/create” className=”nav-link”>Create Exercise Log</Link>

</li>

<li className=”navbar-item”>

<Link to=”/user” className=”nav-link”>Create User</Link>

</li>

</ul>

</div>

</nav>

);

}

}

  1. Now, create four files in the components directory with the following and create stubs for these components:

 

  • exercises-list.component.js

 

import React, { Component } from ‘react’;

 

export default class ExercisesList extends Component {

render() {

return (

<div>

<p>You are on the Exercises List component!</p>

</div>

)

}

}

  • Edit-exercise.component.js

import React, { Component } from ‘react’;

export default class EditExercise extends Component {

render() {

return (

<div>

<p>You are on the Edit Exercise component!</p>

</div>

)

}

}

  • create-exercise.component.js

 

import React, { Component } from ‘react’;

 

export default class CreateExercise extends Component {

render() {

return (

<div>

<p>You are on the Create Exercise component!</p>

</div>

)

}

}

 

  • create-user.component.js

 

import React, { Component } from ‘react’;

 

export default class CreateUser extends Component {

render() {

return (

<div>

<p>You are on the Create User component!</p>

</div>

)

}

}

 

Adding the Create Exercise component

  1. Add a constructor to ‘component.js’. Assign an object to ‘this.state’ as follows:

 

constructor(props) {

super(props);

this.state = {

username: ”,

description: ”,

duration: 0,

date: new Date(),

users: []

}

}

 

  1. Add methods to update the state properties:

 

onChangeUsername(e) {

this.setState({

username: e.target.value

});

}

onChangeDescription(e) {

this.setState({

description: e.target.value

});

}

onChangeDuration(e) {

this.setState({

duration: e.target.value

});

}

onChangeDate(date) {

this.setState({

date: date

});

}

 

  1. After this, add one method to handle the submit event form:

 

onSubmit(e) {

e.preventDefault();

const exercise = {

username: this.state.username,

description: this.state.description,

duration: this.state.duration,

date: this.state.date,

};

console.log(exercise);

window.location = ‘/’;

}

  1. Next, to make sure ‘this’ works properly, add the following lines to the constructor:

 

this.onChangeUsername = this.onChangeUsername.bind(this);

this.onChangeDescription = this.onChangeDescription.bind(this);

this.onChangeDuration = this.onChangeDuration.bind(this);

this.onChangeDate = this.onChangeDate.bind(this);

this.onSubmit = this.onSubmit.bind(this);

 

  1. After the constructor, add the following method:

 

componentDidMount() {

this.setState({

users: [‘test user’],

username: ‘test user’

});

}

  1. The complete file, after adding the form code is as follows:

 

import React, { Component } from ‘react’;

import DatePicker from ‘react-datepicker’;

import “react-datepicker/dist/react-datepicker.css”;

 

export default class CreateExercise extends Component {

constructor(props) {

super(props);

 

this.onChangeUsername = this.onChangeUsername.bind(this);

this.onChangeDescription = this.onChangeDescription.bind(this);

this.onChangeDuration = this.onChangeDuration.bind(this);

this.onChangeDate = this.onChangeDate.bind(this);

this.onSubmit = this.onSubmit.bind(this);

 

this.state = {

username: ”,

description: ”,

duration: 0,

date: new Date(),

users: []

}

}

 

componentDidMount() {

this.setState({

users: [‘test user’],

username: ‘test user’

});

}

 

onChangeUsername(e) {

this.setState({

username: e.target.value

});

}

 

onChangeDescription(e) {

this.setState({

description: e.target.value

});

}

 

onChangeDuration(e) {

this.setState({

duration: e.target.value

});

}

 

onChangeDate(date) {

this.setState({

date: date

});

}

 

onSubmit(e) {

e.preventDefault();

 

const exercise = {

username: this.state.username,

description: this.state.description,

duration: this.state.duration,

date: this.state.date,

};

 

console.log(exercise);

 

window.location = ‘/’;

}

 

render() {

return (

<div>

<h3>Create New Exercise Log</h3>

<form onSubmit={this.onSubmit}>

<div className=”form-group”>

<label>Username: </label>

<select ref=”userInput”

required

className=”form-control”

value={this.state.username}

onChange={this.onChangeUsername}>

{

this.state.users.map(function(user) {

return <option

key={user}

value={user}>{user}

</option>;

})

}

</select>

</div>

<div className=”form-group”>

<label>Description: </label>

<input  type=”text”

required

className=”form-control”

value={this.state.description}

onChange={this.onChangeDescription}

/>

</div>

<div className=”form-group”>

<label>Duration (in minutes): </label>

<input

type=”text”

className=”form-control”

value={this.state.duration}

onChange={this.onChangeDuration}

/>

</div>

<div className=”form-group”>

<label>Date: </label>

<div>

<DatePicker

selected={this.state.date}

onChange={this.onChangeDate}

/>

</div>

</div>

 

<div className=”form-group”>

<input type=”submit” value=”Create Exercise Log” className=”btn btn-primary” />

</div>

</form>

</div>

)

}

}

Adding the Create User component

  1. After the Create Exercise component, you need to create the Create User component. For this, just after the line ‘export default class CreateUser extends Component {‘, add the following constructor:

constructor(props) {

super(props);

this.onChangeUsername = this.onChangeUsername.bind(this);

this.onSubmit = this.onSubmit.bind(this);

this.state = {

username: ”

};

}

  1. Next, add the methods to change the username and submit the form.

onChangeUsername(e) {

this.setState({

username: e.target.value

});

}

onSubmit(e) {

e.preventDefault();

const newUser = {

username: this.state.username,

};

console.log(newUser);

this.setState({

username: ”

})

}

  1. Delete the current code in the return statement and add:

<div>

<h3>Create New User</h3>

<form onSubmit={this.onSubmit}>

<div className=”form-group”>

<label>Username: </label>

<input  type=”text”

required

className=”form-control”

value={this.state.username}

onChange={this.onChangeUsername}

/>

</div>

<div className=”form-group”>

<input type=”submit” value=”Create User” className=”btn btn-primary” />

</div>

</form>

</div>

Connecting Frontend and Backend

  1. Install Axios library to send HTTP requests to the backend using the following command.

$ npm install axios

 

  1. After ‘import React’ line in create-user.component.ts, add the following:

import axios from ‘axios’;

 

  1. To connect the code to the backend, onSubmit method add the following after ‘log(newUser);’

 

axios.post(‘http://localhost:5000/users/add’, newUser)

.then(res => console.log(res.data));

 

  1. Create a new user by directing the browser to ‘localhost:3000/user’. You should be able to see the user you have just added using Insomnia or MongoDB Atlas.

 

Completing the ‘CreateExercise component’ Implementation

  1. After the ‘import React’ component, add the following line:

 

import axios from ‘axios’;

  1. Add the following line after ‘log(exercise);’ in the onSubmit method.

axios.post(‘http://localhost:5000/exercises/add’, exercise)

.then(res => console.log(res.data));

  1. Update the componentWillMount() method in this file. For this, delete the current content and add the following code:

axios.get(‘http://localhost:5000/users/’)

.then(response => {

if (response.data.length > 0) {

this.setState({

users: response.data.map(user => user.username),

username: response.data[0].username

});

}

})

.catch((error) => {

console.log(error);

})

  1. Test this by adding an exercise and checking Insomnia or MongoDB dashboard.
  2. Next, after ‘import React’ line at the beginning of ‘exercises-list.component.js’, add:

import { Link } from ‘react-router-dom’;

import axios from ‘axios’;

  1. Add a constructor after ‘export default class ExercisesList extends Component {‘ to initialize the state with empty exercises array and bind this to the method.

constructor(props) {

super(props);

this.deleteExercise = this.deleteExercise.bind(this);

this.state = {exercises: []};

}

  1. Get the list of exercises by adding the following after the constructor:

componentDidMount() {

axios.get(‘http://localhost:5000/exercises/’)

.then(response => {

this.setState({ exercises: response.data });

})

.catch((error) => {

console.log(error);

})

}

  1. Add the following method to delete exercises:

deleteExercise(id) {

axios.delete(‘http://localhost:5000/exercises/’+id)

.then(res => console.log(res.data));

this.setState({

exercises: this.state.exercises.filter(el => el._id !== id)

})

}

  1. Change the ‘return’ statement of the render function with the following command:

<div>

<h3>Logged Exercises</h3>

<table className=”table”>

<thead className=”thead-light”>

<tr>

<th>Username</th>

<th>Description</th>

<th>Duration</th>

<th>Date</th>

<th>Actions</th>

</tr>

</thead>

<tbody>

{ this.exerciseList() }

</tbody>

</table>

</div>

  1. Directly above the render function add the following code:

exerciseList() {

return this.state.exercises.map(currentexercise => {

return <Exercise exercise={currentexercise} deleteExercise={this.deleteExercise} key={currentexercise._id}/>;

})

}

  1. After the imports statement, at the top of the file:

const Exercise = props => (

<tr>

<td>{props.exercise.username}</td>

<td>{props.exercise.description}</td>

<td>{props.exercise.duration}</td>

<td>{props.exercise.date.substring(0,10)}</td>

<td>

<Link to={“/edit/”+props.exercise._id}>edit</Link> | <a href=”#” onClick={() => { props.deleteExercise(props.exercise._id) }}>delete</a>

</td>

</tr>

)

  1. Next, you need to implement the EditExercise component. Update the edit-exercise.component.js to the following:

import React, { Component } from ‘react’;

import DatePicker from ‘react-datepicker’;

import “react-datepicker/dist/react-datepicker.css”;

import axios from ‘axios’;

export default class EditExercise extends Component {

constructor(props) {

super(props);

this.onChangeUsername = this.onChangeUsername.bind(this);

this.onChangeDescription = this.onChangeDescription.bind(this);

this.onChangeDuration = this.onChangeDuration.bind(this);

this.onChangeDate = this.onChangeDate.bind(this);

this.onSubmit = this.onSubmit.bind(this);

this.state = {

username: ”,

description: ”,

duration: 0,

date: new Date(),

users: []

}

}

componentDidMount() {

axios.get(‘http://localhost:5000/exercises/’+this.props.match.params.id)

.then(response => {

this.setState({

username: response.data.username,

description: response.data.description,

duration: response.data.duration,

date: new Date(response.data.date)

})

})

.catch(function (error) {

console.log(error);

})

axios.get(‘http://localhost:5000/users/’)

.then(response => {

this.setState({ users: response.data.map(user => user.username) });

})

.catch((error) => {

console.log(error);

})

}

onChangeUsername(e) {

this.setState({

username: e.target.value

});

}

onChangeDescription(e) {

this.setState({

description: e.target.value

});

}

onChangeDuration(e) {

this.setState({

duration: e.target.value

});

}

onChangeDate(date) {

this.setState({

date: date

});

}

 

onSubmit(e) {

e.preventDefault();

 

const exercise = {

username: this.state.username,

description: this.state.description,

duration: this.state.duration,

date: this.state.date,

};

console.log(exercise);

axios.post(‘http://localhost:5000/exercises/update/’+this.props.match.params.id, exercise)

.then(res => console.log(res.data));

window.location = ‘/’;

}

 

render() {

return (

<div>

<h3>Edit Exercise Log</h3>

<form onSubmit={this.onSubmit}>

<div className=”form-group”>

<label>Username: </label>

<select ref=”userInput”

className=”form-control”

value={this.state.username}

onChange={this.onChangeUsername}>

{

this.state.users.map(function(user) {

return <option

key={user}

value={user}>{user}

</option>;

})

}

</select>

</div>

<div className=”form-group”>

<label>Description: </label>

<input  type=”text”

required

className=”form-control”

value={this.state.description}

onChange={this.onChangeDescription}

/>

</div>

<div className=”form-group”>

<label>Duration (in minutes): </label>

<input

type=”text”

className=”form-control”

value={this.state.duration}

onChange={this.onChangeDuration}

/>

</div>

<div className=”form-group”>

<label>Date: </label>

<DatePicker

selected={this.state.date}

onChange={this.onChangeDate}

/>

</div>

 

<div className=”form-group”>

<input type=”submit” value=”Edit Exercise Log” className=”btn btn-primary” />

</div>

</form>

</div>

)

}

}

So, now you have built an operational MERN exercise tracker using MongoDB Atlas.

If you want to develop robust applications in the MERN stack, you need not have extensive knowledge of many Programming Languages, instead only be proficient in JSON and JavaScript. Getting trained at a Bootcamp will help in strengthening your coding and development skills. Under the guidance of the competent trainers, you will be able to grab many lucrative career opportunities.

If you are looking for the best Bootcamp to understand the best practices, current industry standards, and to become adept in the MERN Stack programming language, then SynergisticIT is the right place for you. Register today!

Leave a Comment