parimal.dev

Notes on MERN Stack

The following post is a bunch of notes I will take while learning the MERN stack from this tutorial. I am learning MERN stack to apply for an intern role which has a “challenge” to create a little demo app using React and Node.js.

MERN Stack

Apart from these, the tutorial will also use Mongoose, a simple schema-based solution to model application data.

Database terminology

Relational MongoDB
Database Database
Table Collection
Row Document
Index Index
Join $lookup
Foreign Reference

MongoDB setup

MongoDB stores data on disk in BSON format, or binary JSON. We will use MongoDB Atlas which is cloud based, instead of installing and using it locally. A note here is that I can get some free credits of MongoDB Atlas using my Github Student Pack.

Setting up code

First verify if node is installed

node -v

It is installed on my machine, check.

Next we create the react project by using npx command, which runs create-react-app without installing it first.(confusing statement but whatever).

npx create-react-app mern-exercise-tracker

Enter the mern-exercise-tracker directory and create a backend folder.

cd mern-exercise-tracker
mkdir backend

Now we create a package.json file (no idea what that means) -

npm init -y 

Now we install a bunch of dependencies -

npm install express cors mongoose dotenv

Now we install one last package globally -

npm install -g nodemon

Now we create a new file called server.js in the backend directory, and the tutorial copy-pastes some javascript code.

 1const express = require('express');
 2const cors = require('cors');
 3
 4require('dotenv').config();
 5
 6const app = express();
 7const port = process.env.PORT || 5000;
 8
 9app.use(cors());
10app.use(express.json());
11
12app.listen(port, () => {
13    console.log('Server is running on port: ${port}');
14});

Now we can start the server using the command -

nodemon server

And it is working. Moving right along.

Next we add the database connection code to server.js, and it looks like this -

 1const express = require('express');
 2const cors = require('cors');
 3const mongoose = require('mongoose');
 4
 5require('dotenv').config();
 6
 7const app = express();
 8const port = process.env.PORT || 5000;
 9
10app.use(cors());
11app.use(express.json());
12
13const uri = process.env.ATLAS_URI;
14mongoose.connect(uri, {useNewUrlParser: true, useCreateIndex: true});
15const connection = mongoose.connection;
16connection.once('open', () => {
17    console.log("MongoDB database connection established successfully");
18})
19
20app.listen(port, () => {
21    console.log('Server is running on port: ${port}');
22});

The uri is an environment variable which we get from MondoDB ATLAS. This goes to a .env file in the backend directory. NOTE: Replace ‘’ with the password of DB admin, and make sure to replace the brackets as well.

** Setting up MongoDB ATLAS

Just visit the MongoDB ATLAS website, create a free account and use the free tier one. Create a new cluster and copy the connection string.

Next we create a new directy in the backend directory for the database models of our app, along with the files for those models.

mkdir models
cd models
touch exercise.model.js
touch user.model.js

Time to copy paste more code!

For file user.model.js -

 1const mongoose = require('mongoose');
 2
 3const Schema = mongoose.Schema;
 4
 5const userSchema = new Schema({
 6    username: {
 7	type: String,
 8	required: true,
 9	unique: true,
10	trim: true,
11	minlength: 3
12    },
13}, {
14    timestamps: true, 
15});
16
17const User = mongoose.model('User', userSchema);
18
19module.exports = User;

For file exercise.model.js -

 1const mongoose = require('mongoose');
 2
 3const Schema = mongoose.Schema;
 4
 5const exerciseSchema = new Schema({
 6    username: { type: String, required: true },
 7    description: { type: String, required: true},
 8    duration: { type: Number, required: true},
 9    date: { type: Date, required: true},
10}, {
11    timestamps: true, 
12});
13
14const Exercise = mongoose.model('Exercise', exerciseSchema);
15
16module.exports = Exercise;

Now we need to add the API endpoint routes so that the server can be used to perform the CRUD applications (create, read, update, delete). Inside the backend folder, create another folder called routes, and create 2 files called exercises.js and users.js.

mkdir routes
cd routes
touch exercises.js users.js

Before moving on, we need to tell the server to use the files we just created.

Add the following lines into server.js -

1const exercisesRouter = require('./routes/exercises');
2const usersRouter = require('./routes/users');
3
4app.use('/exercises', exercisesRouter);
5app.use('/users', usersRouter);

Next we edit the users.js file for routing -

 1const router = require('express').Router();
 2let User = require('../models/user.model');
 3
 4router.route('/').get((req, res) => {
 5    User.find()
 6	.then(users => res.json(users))
 7	.catch(err => res.status(400).json('Error: ' + err));
 8});
 9
10router.route('/add').post((req, res) => {
11    const username = req.body.username;
12
13    const newUser = new User({username});
14
15    newUser.save()
16	.then(() => res.json('User added!'))
17	.catch(err => res.status(400).json('Error: ' + err));
18});
19
20module.exports = router;

Since I have already worked with Django and PHP before, I understand all this code, exactly what they are doing so not gonna write any notes here.

Now time to edit the exercises.js file -

 1const router = require('express').Router();
 2let Exercise = require('../models/exercise.model');
 3
 4router.route('/').get((req, res) => {
 5    Exercise.find()
 6	.then(exercises => res.json(exercises))
 7	.catch(err => res.status(400).json('Error: ' + err));
 8});
 9
10router.route('/add').post((req, res) => {
11    const username = req.body.username;
12    const description = req.body.description;
13    const duration = Number(req.body.duration);
14    const date = Date.parse(req.body.date);
15
16    const newExercise = new Exercise({
17	username,
18	description,
19	duration,
20	date
21    });
22    
23    newExercise.save()
24	.then(() => res.json('Exercise added!'))
25	.catch(err => res.status(400).json('Error: ' + err));
26});
27
28module.exports = router;

Testing the server API

Download the insomnia API testing app using the following command -

sudo snap install insomnia

To test if the API is working correctly, just create a new POST or GET request and send the data accordingly. It’s working fine till here.

Adding routes to exercises.js

We need exercises.js to perform more operations than simply adding exercises. Add the following lines to exercises.js -

 1router.route('/:id').get((req, res) => {
 2  Exercise.findById(req.params.id)
 3    .then(exercise => res.json(exercise))
 4    .catch(err => res.status(400).json('Error: ' + err));
 5});
 6router.route('/:id').delete((req, res) => {
 7  Exercise.findByIdAndDelete(req.params.id)
 8    .then(() => res.json('Exercise deleted.'))
 9    .catch(err => res.status(400).json('Error: ' + err));
10});
11router.route('/update/:id').post((req, res) => {
12  Exercise.findById(req.params.id)
13    .then(exercise => {
14      exercise.username = req.body.username;
15      exercise.description = req.body.description;
16      exercise.duration = Number(req.body.duration);
17      exercise.date = Date.parse(req.body.date);
18
19      exercise.save()
20        .then(() => res.json('Exercise updated!'))
21        .catch(err => res.status(400).json('Error: ' + err));
22    })
23    .catch(err => res.status(400).json('Error: ' + err));
24});

Running the server after this gives an error saying cannot connect to MongoDB ATLAS. This has to do with me using a different IP address since the database grants access to specific IP address which we tell while creating the cluster.

To solve this just go to MongoDB ATLAS site, in the left side navigation links go to Network Access and add your current IP address.

All the new routes works fine when testing using insomnia. Time to move on to frontend.

Frontend

React is a Javascript library for building user interfaces. We use components to tell React what we wish to see on the screen. When the data changes, react automatically re-render our components.

Starting the development server -

npm start

Also we need bootstrap CSS to make styling for our project easier.

npm install bootstrap

Next we need to setup React Router.

npm install react-router-dom

So the tutorial has…pasted many lines of code of React…so I will do the same and then understand them.

So every component needs a .js file, and all this goes inside the main App.js file.

App.js file will look like this -

 1import React from 'react';
 2import { BrowserRouter as Router, Route } from "react-router-dom";
 3import "bootstrap/dist/css/bootstrap.min.css";
 4
 5import Navbar from "./components/navbar.component"
 6import ExercisesList from "./components/exercises-list.component";
 7import EditExercise from "./components/edit-exercise.component";
 8import CreateExercise from "./components/create-exercise.component";
 9import CreateUser from "./components/create-user.component";
10
11function App() {
12  return (
13    <Router>
14      <div className="container">
15        <Navbar />
16        <br/>
17        <Route path="/" exact component={ExercisesList} />
18        <Route path="/edit/:id" component={EditExercise} />
19        <Route path="/create" component={CreateExercise} />
20        <Route path="/user" component={CreateUser} />
21      </div>
22    </Router>
23  );
24}
25
26export default App;

Now we make a new folder inside the /src directory (where App.js exists) and make 5 new .js files for each component. These are -

Code for each file -

navbar.component.js -

 1import React, { Component } from 'react';
 2import { Link } from 'react-router-dom';
 3
 4export default class Navbar extends Component {
 5
 6  render() {
 7    return (
 8      <nav className="navbar navbar-dark bg-dark navbar-expand-lg">
 9        <Link to="/" className="navbar-brand">ExcerTracker</Link>
10        <div className="collpase navbar-collapse">
11        <ul className="navbar-nav mr-auto">
12          <li className="navbar-item">
13          <Link to="/" className="nav-link">Exercises</Link>
14          </li>
15          <li className="navbar-item">
16          <Link to="/create" className="nav-link">Create Exercise Log</Link>
17          </li>
18          <li className="navbar-item">
19          <Link to="/user" className="nav-link">Create User</Link>
20          </li>
21        </ul>
22        </div>
23      </nav>
24    );
25  }
26}

For testing the site now, we will add some stub code to other components -

exercises-list.component.js:

 1import React, { Component } from 'react';
 2
 3export default class ExercisesList extends Component {
 4  render() {
 5    return (
 6      <div>
 7        <p>You are on the Exercises List component!</p>
 8      </div>
 9    )
10  }
11}

edit-exercise.component.js:

 1import React, { Component } from 'react';
 2
 3export default class EditExercise extends Component {
 4  render() {
 5    return (
 6      <div>
 7        <p>You are on the Edit Exercise component!</p>
 8      </div>
 9    )
10  }
11}

create-exercise.component.js:

 1import React, { Component } from 'react';
 2
 3export default class CreateExercise extends Component {
 4  render() {
 5    return (
 6      <div>
 7        <p>You are on the Create Exercise component!</p>
 8      </div>
 9    )
10  }
11}

create-user.component.js:

 1import React, { Component } from 'react';
 2
 3export default class CreateUser extends Component {
 4  render() {
 5    return (
 6      <div>
 7        <p>You are on the Create User component!</p>
 8      </div>
 9    )
10  }
11}

After this, we I run the server it works perfectly. Now time to edit the individual components -

create-exercise.component.js

  1import React, { Component } from 'react';
  2import DatePicker from 'react-datepicker';
  3import "react-datepicker/dist/react-datepicker.css";
  4
  5export default class CreateExercise extends Component {
  6  constructor(props) {
  7    super(props);
  8
  9    this.onChangeUsername = this.onChangeUsername.bind(this);
 10    this.onChangeDescription = this.onChangeDescription.bind(this);
 11    this.onChangeDuration = this.onChangeDuration.bind(this);
 12    this.onChangeDate = this.onChangeDate.bind(this);
 13    this.onSubmit = this.onSubmit.bind(this);
 14
 15    this.state = {
 16      username: '',
 17      description: '',
 18      duration: 0,
 19      date: new Date(),
 20      users: []
 21    }
 22  }
 23
 24  componentDidMount() {
 25    this.setState({ 
 26      users: ['test user'],
 27      username: 'test user'
 28    });
 29  }
 30
 31  onChangeUsername(e) {
 32    this.setState({
 33      username: e.target.value
 34    });
 35  }
 36
 37  onChangeDescription(e) {
 38    this.setState({
 39      description: e.target.value
 40    });
 41  }
 42
 43  onChangeDuration(e) {
 44    this.setState({
 45      duration: e.target.value
 46    });
 47  }
 48
 49  onChangeDate(date) {
 50    this.setState({
 51      date: date
 52    });
 53  }
 54
 55  onSubmit(e) {
 56    e.preventDefault();
 57  
 58    const exercise = {
 59      username: this.state.username,
 60      description: this.state.description,
 61      duration: this.state.duration,
 62      date: this.state.date,
 63    };
 64  
 65    console.log(exercise);
 66    
 67    window.location = '/';
 68  }
 69
 70  render() {
 71    return (
 72      <div>
 73        <h3>Create New Exercise Log</h3>
 74        <form onSubmit={this.onSubmit}>
 75          <div className="form-group"> 
 76            <label>Username: </label>
 77            <select ref="userInput"
 78                required
 79                className="form-control"
 80                value={this.state.username}
 81                onChange={this.onChangeUsername}>
 82                {
 83                  this.state.users.map(function(user) {
 84                    return <option 
 85                      key={user}
 86                      value={user}>{user}
 87                      </option>;
 88                  })
 89                }
 90            </select>
 91          </div>
 92          <div className="form-group"> 
 93            <label>Description: </label>
 94            <input  type="text"
 95                required
 96                className="form-control"
 97                value={this.state.description}
 98                onChange={this.onChangeDescription}
 99                />
100          </div>
101          <div className="form-group">
102            <label>Duration (in minutes): </label>
103            <input 
104                type="text" 
105                className="form-control"
106                value={this.state.duration}
107                onChange={this.onChangeDuration}
108                />
109          </div>
110          <div className="form-group">
111            <label>Date: </label>
112            <div>
113              <DatePicker
114                selected={this.state.date}
115                onChange={this.onChangeDate}
116              />
117            </div>
118          </div>
119
120          <div className="form-group">
121            <input type="submit" value="Create Exercise Log" className="btn
122            btn-primary" />
123          </div>
124        </form>
125      </div>
126    )
127  }
128}

Now time to edit create-user.component.js:

 1import React, { Component } from 'react';
 2
 3export default class CreateUser extends Component {
 4  constructor(props) {
 5  super(props);
 6  this.onChangeUsername = this.onChangeUsername.bind(this);
 7  this.onSubmit = this.onSubmit.bind(this);
 8  this.state = {
 9    username: ''
10  };
11  }
12    
13  onChangeUsername(e) {
14  this.setState({
15    username: e.target.value
16  });
17}
18onSubmit(e) {
19  e.preventDefault();
20  const newUser = {
21    username: this.state.username,
22  };
23  console.log(newUser);
24  
25  this.setState({
26    username: ''
27  })
28}
29
30  render() {
31    return (
32  <div>
33  <h3>Create New User</h3>
34  <form onSubmit={this.onSubmit}>
35    <div className="form-group"> 
36      <label>Username: </label>
37      <input  type="text"
38          required
39          className="form-control"
40          value={this.state.username}
41          onChange={this.onChangeUsername}
42          />
43    </div>
44    <div className="form-group">
45      <input type="submit" value="Create User" className="btn btn-primary" />
46    </div>
47  </form>
48</div>
49    )
50  }
51}

Connecting front-end and back-end

We’ll use the Axios library to send HTTP requests to our backend. Install it with the following command in your terminal:

npm install axios

Now we add the following line to create-user.component.js -

import axios from 'axios';

Add the following line after console.log(newUser) in the onSubmit method -

axios.post('http://localhost:5000/users/add', newUser)
.then(res => console.log(res.data));

The axios.post method sends an HTTP POST request to the backend endpoint http://localhost:5000/users/add. This endpoint is expecting a JSON object in the request body so we passed in the newUser object as a second argument.

After this we can add new users using the frontend.

Now time to complete the create-exercise.js file -

  1import React, { Component } from 'react';
  2import axios from 'axios';
  3import DatePicker from 'react-datepicker';
  4import "react-datepicker/dist/react-datepicker.css";
  5
  6export default class CreateExercise extends Component {
  7  constructor(props) {
  8    super(props);
  9
 10    this.onChangeUsername = this.onChangeUsername.bind(this);
 11    this.onChangeDescription = this.onChangeDescription.bind(this);
 12    this.onChangeDuration = this.onChangeDuration.bind(this);
 13    this.onChangeDate = this.onChangeDate.bind(this);
 14    this.onSubmit = this.onSubmit.bind(this);
 15
 16    this.state = {
 17      username: '',
 18      description: '',
 19      duration: 0,
 20      date: new Date(),
 21      users: []
 22    }
 23  }
 24
 25    componentWillMount() {
 26	axios.get('http://localhost:5000/users/')
 27  .then(response => {
 28    if (response.data.length > 0) {
 29      this.setState({ 
 30        users: response.data.map(user => user.username),
 31        username: response.data[0].username
 32      });
 33    }
 34  })
 35  .catch((error) => {
 36    console.log(error);
 37  })
 38  }
 39
 40  onChangeUsername(e) {
 41    this.setState({
 42      username: e.target.value
 43    });
 44  }
 45
 46  onChangeDescription(e) {
 47    this.setState({
 48      description: e.target.value
 49    });
 50  }
 51
 52  onChangeDuration(e) {
 53    this.setState({
 54      duration: e.target.value
 55    });
 56  }
 57
 58  onChangeDate(date) {
 59    this.setState({
 60      date: date
 61    });
 62  }
 63
 64  onSubmit(e) {
 65    e.preventDefault();
 66  
 67    const exercise = {
 68      username: this.state.username,
 69      description: this.state.description,
 70      duration: this.state.duration,
 71      date: this.state.date,
 72    };
 73  
 74    console.log(exercise);
 75    axios.post('http://localhost:5000/exercises/add', exercise)
 76   .then(res => console.log(res.data));
 77    window.location = '/';
 78  }
 79
 80  render() {
 81    return (
 82      <div>
 83        <h3>Create New Exercise Log</h3>
 84        <form onSubmit={this.onSubmit}>
 85          <div className="form-group"> 
 86            <label>Username: </label>
 87            <select ref="userInput"
 88                required
 89                className="form-control"
 90                value={this.state.username}
 91                onChange={this.onChangeUsername}>
 92                {
 93                  this.state.users.map(function(user) {
 94                    return <option 
 95                      key={user}
 96                      value={user}>{user}
 97                      </option>;
 98                  })
 99                }
100            </select>
101          </div>
102          <div className="form-group"> 
103            <label>Description: </label>
104            <input  type="text"
105                required
106                className="form-control"
107                value={this.state.description}
108                onChange={this.onChangeDescription}
109                />
110          </div>
111          <div className="form-group">
112            <label>Duration (in minutes): </label>
113            <input 
114                type="text" 
115                className="form-control"
116                value={this.state.duration}
117                onChange={this.onChangeDuration}
118                />
119          </div>
120          <div className="form-group">
121            <label>Date: </label>
122            <div>
123              <DatePicker
124                selected={this.state.date}
125                onChange={this.onChangeDate}
126              />
127            </div>
128          </div>
129
130          <div className="form-group">
131            <input type="submit" value="Create Exercise Log" className="btn
132            btn-primary" />
133          </div>
134        </form>
135      </div>
136    )
137  }
138}

Now we’ll complete the ExercisesList component.

The exercises-list.component.js will look like this -

 1import React, { Component } from 'react';
 2import { Link } from 'react-router-dom';
 3import axios from 'axios';
 4
 5const Exercise = props => (
 6  <tr>
 7    <td>{props.exercise.username}</td>
 8    <td>{props.exercise.description}</td>
 9    <td>{props.exercise.duration}</td>
10    <td>{props.exercise.date.substring(0,10)}</td>
11    <td>
12      <Link to={"/edit/"+props.exercise._id}>edit</Link> | <a href="#"
13      onClick={() => { props.deleteExercise(props.exercise._id) }}>delete</a>
14    </td>
15  </tr>
16)
17
18export default class ExercisesList extends Component {
19  constructor(props) {
20    super(props);
21
22    this.deleteExercise = this.deleteExercise.bind(this)
23
24    this.state = {exercises: []};
25  }
26
27  componentDidMount() {
28    axios.get('http://localhost:5000/exercises/')
29      .then(response => {
30        this.setState({ exercises: response.data })
31      })
32      .catch((error) => {
33        console.log(error);
34      })
35  }
36
37  deleteExercise(id) {
38    axios.delete('http://localhost:5000/exercises/'+id)
39      .then(response => { console.log(response.data)});
40
41    this.setState({
42      exercises: this.state.exercises.filter(el => el._id !== id)
43    })
44  }
45
46  exerciseList() {
47    return this.state.exercises.map(currentexercise => {
48      return <Exercise exercise={currentexercise}
49      deleteExercise={this.deleteExercise} key={currentexercise._id}/>;
50    })
51  }
52
53  render() {
54    return (
55      <div>
56        <h3>Logged Exercises</h3>
57        <table className="table">
58          <thead className="thead-light">
59            <tr>
60              <th>Username</th>
61              <th>Description</th>
62              <th>Duration</th>
63              <th>Date</th>
64              <th>Actions</th>
65            </tr>
66          </thead>
67          <tbody>
68            { this.exerciseList() }
69          </tbody>
70        </table>
71      </div>
72    )
73  }
74}

And finally, the code for edit-exercise.component.js -

  1import React, { Component } from 'react';
  2import axios from 'axios';
  3import DatePicker from 'react-datepicker';
  4import "react-datepicker/dist/react-datepicker.css";
  5
  6export default class EditExercise extends Component {
  7  constructor(props) {
  8    super(props);
  9
 10    this.onChangeUsername = this.onChangeUsername.bind(this);
 11    this.onChangeDescription = this.onChangeDescription.bind(this);
 12    this.onChangeDuration = this.onChangeDuration.bind(this);
 13    this.onChangeDate = this.onChangeDate.bind(this);
 14    this.onSubmit = this.onSubmit.bind(this);
 15
 16    this.state = {
 17      username: '',
 18      description: '',
 19      duration: 0,
 20      date: new Date(),
 21      users: []
 22    }
 23  }
 24
 25  componentDidMount() {
 26    axios.get('http://localhost:5000/exercises/'+this.props.match.params.id)
 27      .then(response => {
 28        this.setState({
 29          username: response.data.username,
 30          description: response.data.description,
 31          duration: response.data.duration,
 32          date: new Date(response.data.date)
 33        })   
 34      })
 35      .catch(function (error) {
 36        console.log(error);
 37      })
 38
 39    axios.get('http://localhost:5000/users/')
 40      .then(response => {
 41        if (response.data.length > 0) {
 42          this.setState({
 43            users: response.data.map(user => user.username),
 44          })
 45        }
 46      })
 47      .catch((error) => {
 48        console.log(error);
 49      })
 50
 51  }
 52
 53  onChangeUsername(e) {
 54    this.setState({
 55      username: e.target.value
 56    })
 57  }
 58
 59  onChangeDescription(e) {
 60    this.setState({
 61      description: e.target.value
 62    })
 63  }
 64
 65  onChangeDuration(e) {
 66    this.setState({
 67      duration: e.target.value
 68    })
 69  }
 70
 71  onChangeDate(date) {
 72    this.setState({
 73      date: date
 74    })
 75  }
 76
 77  onSubmit(e) {
 78    e.preventDefault();
 79
 80    const exercise = {
 81      username: this.state.username,
 82      description: this.state.description,
 83      duration: this.state.duration,
 84      date: this.state.date
 85    }
 86
 87    console.log(exercise);
 88
 89    axios.post('http://localhost:5000/exercises/update/' +
 90    this.props.match.params.id, exercise)
 91      .then(res => console.log(res.data));
 92
 93    window.location = '/';
 94  }
 95
 96  render() {
 97    return (
 98    <div>
 99      <h3>Edit Exercise Log</h3>
100      <form onSubmit={this.onSubmit}>
101        <div className="form-group"> 
102          <label>Username: </label>
103          <select ref="userInput"
104              required
105              className="form-control"
106              value={this.state.username}
107              onChange={this.onChangeUsername}>
108              {
109                this.state.users.map(function(user) {
110                  return <option 
111                    key={user}
112                    value={user}>{user}
113                    </option>;
114                })
115              }
116          </select>
117        </div>
118        <div className="form-group"> 
119          <label>Description: </label>
120          <input  type="text"
121              required
122              className="form-control"
123              value={this.state.description}
124              onChange={this.onChangeDescription}
125              />
126        </div>
127        <div className="form-group">
128          <label>Duration (in minutes): </label>
129          <input 
130              type="text" 
131              className="form-control"
132              value={this.state.duration}
133              onChange={this.onChangeDuration}
134              />
135        </div>
136        <div className="form-group">
137          <label>Date: </label>
138          <div>
139            <DatePicker
140              selected={this.state.date}
141              onChange={this.onChangeDate}
142            />
143          </div>
144        </div>
145
146        <div className="form-group">
147          <input type="submit" value="Edit Exercise Log" className="btn
148          btn-primary" />
149        </div>
150      </form>
151    </div>
152    )
153  }
154}

And that finally finishes the tutorial and our simple exercise tracker application.

<< Previous Post

|

Next Post >>

#Web-Dev