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
- MongoDB: A document based open source database.
- Express: A web application framework for Node.js
- React: A JavaScript front-end library for building user interfaces.
- Node.js: JavaScript run-time environment that executes JavaScript code outside of a browser.
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
- cors stands for cross-origin resource sharing allows AJAX requests to skip the same origin policy and access resources from remote hosts.
- dotenv loads environment variables from a .env file into a process.env file which somehow makes development simpler.
Now we install one last package globally -
npm install -g nodemon
- nodemon automatically restarts the node application when file changes in the directory are detected.
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 ‘
** 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 -
- navbar.component.js
- exercises-list.component.js
- edit-exercise.component.js
- create-exercise.component.js
- create-user.component.js
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.