Skip to content

Step 5: Project 3 MERN stack implementation

monotiller edited this page May 10, 2022 · 2 revisions

Discussion page

Simple to-do application on MERN web stack

  1. MongoDB: A document-based, No-SQL database used to store application data in a form of documents.
  2. ExpressJS: A server side Web Application framework for Node.js.
  3. ReactJS: A frontend framework developed by Facebook. It is based on JavaScript, used to build User Interface (UI) components.
  4. Node.js: A JavaScript runtime environment. It is used to run JavaScript on a machine rather than in a browser.

Backend configuration

asciicast

Above is a recording of my terminal as I did this step, let me know your thoughts on doing this in the future!

Node was installed using:

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs

The reported versions are Node v12.22.12 and npm 6.14.16

I then created a folder called Todo and ran npm init in there and filled out the form it provided. This is my configuration json:

{
  "name": "todo",
  "version": "1.0.0",
  "description": "A todo application made as part of the Aker Academy",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\""
  },
  "author": "Madeline Marsh",
  "license": "ISC" // Wasn't sure what to put here so I left it at default
}

Install ExpressJS

asciicast

Express was installed using npm install express and it did throw a few warnings but ultimately did install correctly

The dotenv module was installed and threw one warning

But since this wasn't required I was expecting it anyway! Then I opened vim and entered in the index.js code:

const express = require('express');
require('dotenv').config();

const app = express();

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

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "\*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});

app.use((req, res, next) => {
res.send('Welcome to Express');
});

app.listen(port, () => {
console.log(`Server running on port ${port}`)
});

node index.js was then run to start the server, the rules were updated in AWS to allow for connections on port 5000

And to verify I visited it in a web browser:

The routes folder was created and vim was used to create api.js

const express = require ('express');
const router = express.Router();

router.get('/todos', (req, res, next) => {

});

router.post('/todos', (req, res, next) => {

});

router.delete('/todos/:id', (req, res, next) => {

})

module.exports = router;

Models

asciicast

Mongoose was installed withh npm install mongoose. Then a new directory called models was created and inside todo.js was placed with the contents:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

//create schema for todo
const TodoSchema = new Schema({
action: {
type: String,
required: [true, 'The todo text field is required']
}
})

//create model for todo
const Todo = mongoose.model('todo', TodoSchema);

module.exports = Todo;

The contents of the api.js that was in the routes directory was deleted and and replaced with:

const express = require ('express');
const router = express.Router();
const Todo = require('../models/todo');

router.get('/todos', (req, res, next) => {

//this will return all the data, exposing only the id and action field to the client
Todo.find({}, 'action')
.then(data => res.json(data))
.catch(next)
});

router.post('/todos', (req, res, next) => {
if(req.body.action){
Todo.create(req.body)
.then(data => res.json(data))
.catch(next)
}else {
res.json({
error: "The input field is empty"
})
}
});

router.delete('/todos/:id', (req, res, next) => {
Todo.findOneAndDelete({"_id": req.params.id})
.then(data => res.json(data))
.catch(next)
})

module.exports = router;

MongoDB database

I signed up for an account on mongodb.com and allowed access to it by any IP address

The interface has changed a little bit since the instructions were written, I also had to select eu-west-1 as eu-west-2 is not on the free tier. I created a user for access:

Setup took a few minutes:

I then went to browse the collections and clicked "Add My Own Data"

I created a database and a collection:

The connection string was:

DB = 'mongodb+srv://user:<password>@cluster0.il62h.mongodb.net/aker?retryWrites=true&w=majority'

Replced the contents of index.js with the following:

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const routes = require('./routes/api');
const path = require('path');
require('dotenv').config();

const app = express();

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

//connect to the database
mongoose.connect(process.env.DB, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log(`Database connected successfully`))
.catch(err => console.log(err));

//since mongoose promise is depreciated, we overide it with node's promise
mongoose.Promise = global.Promise;

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "\*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});

app.use(bodyParser.json());

app.use('/api', routes);

app.use((err, req, res, next) => {
console.log(err);
next();
});

app.listen(port, () => {
console.log(`Server running on port ${port}`)
});

And then started the server:

Using the API testing tool Postman I sent a message to the database (the same one that was in the writing for this task due to lack of creativity) and it appeared in the database after querying it:

Optional task

To delete a task I simply sent a delete request to the /api/todo/ URL but I added the ID to the end of the end of the URL so it looked like this:

http://3.10.173.224:5000/api/todos/627a90bf5f4dc335f37b88af

Frontend configuration

npx create-react-app client requires node 14 or higher so I updated node version with "n" which is a Node.js version manager.

sudo npm cache clean -f
sudo npm install -g n
sudo n stable

And now I'm running the latest version v16.15.0

Now I can run npx create-react-app client. This ended up taking a few minutes

Concurrently was installed with npm install concurrently --save-dev followed by nodemon npm install nodemon --save-dev.

I then made the changes to package.json:

{
  "name": "todo",
  "version": "1.0.0",
  "description": "A todo application made as part of the Aker Academy",
  "main": "index.js",
  "author": "Madeline Marsh",
  "scripts": {
        "start": "node index.js",
        "start-watch": "nodemon index.js",
        "dev": "concurrently \"npm run start-watch\" \"cd client && npm start\""
        },
  "license": "ISC",
  "dependencies": {
    "create-react-app": "^5.0.1",
    "dotenv": "^16.0.0",
    "mongoose": "^6.3.3"
  },
  "devDependencies": {
    "concurrently": "^7.1.0",
    "nodemon": "^2.0.16"
  }
}

I then moved to the client directory and made the changes to the package.json file which now reads:

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:5000",
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.2.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.1.0",
    "react-dom": "^18.1.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Running npm run dev we get a successful start

As seen in the web browser:

Then I moved into /Todo/client/src/components/ and created three files with the touch command, touch Input.js ListTodo.js Todo.js. Then in Input.js I entered this:

import React, { Component } from 'react';
import axios from 'axios';

class Input extends Component {

state = {
action: ""
}

addTodo = () => {
const task = {action: this.state.action}

    if(task.action && task.action.length > 0){
      axios.post('/api/todos', task)
        .then(res => {
          if(res.data){
            this.props.getTodos();
            this.setState({action: ""})
          }
        })
        .catch(err => console.log(err))
    }else {
      console.log('input field required')
    }

}

handleChange = (e) => {
this.setState({
action: e.target.value
})
}

render() {
let { action } = this.state;
return (
<div>
<input type="text" onChange={this.handleChange} value={action} />
<button onClick={this.addTodo}>add todo</button>
</div>
)
}
}

export default Input

Finally I went back to the client folder and installed axios with npm install axios

Frontend configuration (continued)

In the src/components/ directory I opened ListTodo.js and entered in the following:

import React from 'react';

const ListTodo = ({ todos, deleteTodo }) => {

return (
<ul>
{
todos &&
todos.length > 0 ?
(
todos.map(todo => {
return (
<li key={todo._id} onClick={() => deleteTodo(todo._id)}>{todo.action}</li>
)
})
)
:
(
<li>No todo(s) left</li>
)
}
</ul>
)
}

export default ListTodo

Then in todo.js I entered this:

import React, {Component} from 'react';
import axios from 'axios';

import Input from './Input';
import ListTodo from './ListTodo';

class Todo extends Component {

state = {
todos: []
}

componentDidMount(){
this.getTodos();
}

getTodos = () => {
axios.get('/api/todos')
.then(res => {
if(res.data){
this.setState({
todos: res.data
})
}
})
.catch(err => console.log(err))
}

deleteTodo = (id) => {

    axios.delete(`/api/todos/${id}`)
      .then(res => {
        if(res.data){
          this.getTodos()
        }
      })
      .catch(err => console.log(err))

}

render() {
let { todos } = this.state;

    return(
      <div>
        <h1>My Todo(s)</h1>
        <Input getTodos={this.getTodos}/>
        <ListTodo todos={todos} deleteTodo={this.deleteTodo}/>
      </div>
    )

}
}

export default Todo;

Then back in the src folder logo.svg was deleted with rm -rf logo.svg. Then the contents of App.js was deleted and replaced with this:

import React from 'react';

import Todo from './components/Todo';
import './App.css';

const App = () => {
return (
<div className="App">
<Todo />
</div>
);
}

export default App;

Then App.css was replaced with this:

.App {
text-align: center;
font-size: calc(10px + 2vmin);
width: 60%;
margin-left: auto;
margin-right: auto;
}

input {
height: 40px;
width: 50%;
border: none;
border-bottom: 2px #101113 solid;
background: none;
font-size: 1.5rem;
color: #787a80;
}

input:focus {
outline: none;
}

button {
width: 25%;
height: 45px;
border: none;
margin-left: 10px;
font-size: 25px;
background: #101113;
border-radius: 5px;
color: #787a80;
cursor: pointer;
}

button:focus {
outline: none;
}

ul {
list-style: none;
text-align: left;
padding: 15px;
background: #171a1f;
border-radius: 5px;
}

li {
padding: 15px;
font-size: 1.5rem;
margin-bottom: 15px;
background: #282c34;
border-radius: 5px;
overflow-wrap: break-word;
cursor: pointer;
}

@media only screen and (min-width: 300px) {
.App {
width: 80%;
}

input {
width: 100%
}

button {
width: 100%;
margin-top: 15px;
margin-left: 0;
}
}

@media only screen and (min-width: 640px) {
.App {
width: 60%;
}

input {
width: 50%;
}

button {
width: 30%;
margin-left: 10px;
margin-top: 0;
}
}

Finally index.css was replaced with this:

body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
background-color: #282c34;
color: #787a80;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

In the Todo directory I ran npm run dev. The site loaded successfully, however, there were no entries in the database because I had deleted them in the previous task