In this project, we are going to deploy a simple to-do application on the MERN STACK in AWS CLOUD.
MERN consists of the following:
-
MongoDB : This is a document based, no-SQL database used to store application data in form of document.
-
ExpressJs : This is a server side web application framework for Node.js.
-
ReactJs : This is a frontend framework used to build User Interface (UI) which is based on Javascript.This was developed by Facebook.
-
NodeJs : This is a JavaScript runtime environment. It is used to run JavaScript on a machine rather than in a browser.
To continue with this Project, we need to do the following:
- create an account on AWS.
- we create an instance (virtual machine) by selecting “ubuntu server 20.04 LTS” from Amazon Machine Image(AMI)(free tier).
- we select “t2.micro(free tier eligible)”
- then go to the security group and select “existing security group” review and launch.
How to create an aws free tier account. click here
This launches us into the instance as shown in the screenshot:
We open our terminal and go to the location of the previously downloaded PEM file:
To know how to download PEM File from AWS. Click here.
We connect to the instance from our ubuntu terminal using the command:
$ ssh -i dybran-ec2.pem [email protected]
This automatically connects to the instance
When we are done connecting to the instance, we can proceed with setting up our project.
BACKEND CONFIGURATION
We run the commands:
To update Ubuntu.
$ sudo apt update
To upgrade Ubuntu
$ sudo apt upgrade
Next, we get the location of the Node.js software from the ubuntu repository by running this command:
$ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
After doing the above, we can now install the Node.js and npm by running the command:
$ sudo apt-get install -y nodejs
To check the version the node:
$ node -v
To check the version fo the npm:
$ npm -v
Next we create a directory for our To-Do Project:
$ mkdir Todo
We run the ls command to verify that the directory was created:
$ ls
Change into the newly created directory:
$ cd Todo
Next, we initialize the project so that a new file package.json will be created. This file contains information of the application and the dependencies it nedds to run.
We initialize the project by running the command:
$ npm init
We run the ls command to verify that package.json was created
$ ls
Next we install ExpresJs and create the Routes directory.
INSTALL EXPRESSJS
Express is a framework for NodeJs Express helps to define routes of your application based on HTTP methods and URLs.
To use Express, we install it using npm:
$ npm install express
Now create a file index.js using the command:
$ touch index.js
We run ls to confirm that the index.js file is successfully created.
Next, install the dotenv module by running the command:
$ npm install dotenv
Open the index.js file by running the command:
$ vim index.js
We copy the folowing into it and "save" using ":wq"
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}`)
});
We start our server to see if it works. Before we start the server, we need to be sure we are in the Todo directory.
$ node index.js
If there are no erros, we should see ""Server running on port 5000" in your terminal.
We need to go to our instance security group and edit inbound rules and add rules
We open our browser and put in the URL
http://<PublicIP>:5000
We should see "welcome to Express"
Our To-Do application should be able to do the following:
- create a new task.
- dislay the lists of tasks.
- delcte acompleted task.
We need to create routes that will define various endpoints that the To-do application will depend on.
To create a folder routes, we run the command:
$ mkdir routes
Change into the routes directory
$ cd routes
Now, create a file api.js with the command:
$ touch api.js
Open the file
$ vim api.js
We then copy and paste the code below into the api.js file
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;
"save" and "quit" using ":wq"
CREATING MODELS
We then go ahead to create "Models" since the application will be using MongoDB which is a noSQL database. A Model makes the javascript application interactive. We also use Models to define the database schema . This is important so that we will be able to define the fields stored in each Mongodb document.
The Schema shows how the database will be setup, including other data fields that may not be required to be stored in the database.
To create a Schema and a model, we will need to install mongoose which is a Node.js package that makes working with mongodb easier.
To install Mongoose, we make sure we are in the Todo directory then run the command:
$ npm install mongoose
We go ahead to create a folder models by running the command:
$ mkdir models
Then we chaneg into the model directory:
$ cd models
Inside the models folder, create a file named todo.js
$ touch todo.js
We then open the file and paste the following codes:
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;
We then go to our routes directory and update the api.js file to be able to make use of the new model.
$ cd routes
Open the api.js file
$ vim api.js
We copy and paste the following codes into the file:
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;
Next we setup the MongoDB Database.
SETTING UP THE MONGODB DATABASE
We use MongoDB Database to store our data using mLab which provides Database as a service (DBaas) solution.
To continue, we sign up here.
Follow the sign up process, select AWS as the cloud provider, and choose a region near you.
Go to "Network access", select "Allow access from anywhere". This is not secure but good for testing purposes.
Then change the time of deleting the entry from 6 Hours to 1 Week.
Create a MongoDB database and collection inside mLab by clicking on "database", click on "cluster0" and then open "collections".
In the index.js file, we specified process.env to access environment variables, but we are yet to create this file.
To create the process.env file, we creta a file in Todo directory and name it .env.
$ touch .env
Open the file
$ vim .env
Add the connection string to access the database in it, just as below:
DB = 'mongodb+srv://<username>:<password>@<network-address>/<dbname>?retryWrites=true&w=majority'
Make sure to update "<username, <password, <network-address" and <database according to your setup.
To get the connection string, we click on "cluster0" then click on "connect"
Then we click on "Mongodb drivers-connect your application..."
We then copy then connection string displayed into the .env file.
We need to update the index.js to reflect the use of .env so that Node.js can connect to the database.
To do that we open the index.js file and delete the content using "esc" then ":%d" then "enter".
We then replace then content with the following codes:
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}`)
});
It is more secure to use environment variables to store information so as to separate configuration and secret data from the application, instead of writing connection strings directly inside the index.js application file.
We start our server by running the command:
$ node index.js
If the setup has no errors, we should see " Database connected successfully".
We now have our backend configured, but we need to test it.
To test the backend without the frontend we are going to be using Restful API with the help of an API development client Postman.
To download and install Postman, click here.
Click here to learn how to perform CRUD Operations on Postman.
We test all the API endpoints and make sure they are working. For the endpoints that require body, you should send JSON back with the necessary fields since it’s what we setup in our code.
Now open your Postman, create a POST request to the API
http://<PublicIP>:5000/api/todos
.
This request sends a new task to our To-Do list so the application could store it in the database.
Make sure to set the header "content-type" and "application/json":
Then click on body. In the field below we key in a command that displays as a response on then next field with an id.
Create a GET request to your API on
http://<PublicIP>:5000/api/todos
.
This request retrieves all existing records from out To-do application. The backend requests these records from the database and sends it us back as a response to GET request.
To delete a task – you need to send its ID as a part of DELETE request.
By now you have tested backend part of our To-Do application and have made sure that it supports all three operations we wanted:
- Display a list of tasks – HTTP GET request
- Add a new task to the list – HTTP POST request
- Delete an existing task from the list – HTTP DELETE request
We have successfully created our Backend, now let go create the Frontend.
SETTING UP THE FRONTEND
We will create a user interface for a Web client (browser) to interact with the application via API.
To do this, we will start by running the command in the Todo directory:
$ npx create-react-app client
This will create a new folder in your Todo directory called client, where you will add all the react code.
Running a React Application
There are some dependencies that need to be installed before running the React App.
We Install "concurrently" by running the command:
$ npm install concurrently --save-dev
It is used to run more than one command simultaneously from the same terminal window.
Next, we install "nodemon". This is used to run and monitor the server. If there is any change in the server code, nodemon will restart it automatically and load the new changes.
$ npm install nodemon --save-dev
Goto the Todo directory, open the package.json file.
$ vim package.json
In this file, replace the "scripts" section with the following code:
"scripts": {
"start": "node index.js",
"start-watch": "nodemon index.js",
"dev": "concurrently \"npm run start-watch\" \"cd client && npm start\""
},
Change into the client directory:
$ cd client
Open package.json file:
$ vim apckage.json
Add the key value pair in the package.json file
"proxy": "http://localhost:5000"
The aim of adding the proxy to the file is to make it possible to access the application directly from the browser by simply calling the server url like http://localhost:5000 rather than always including the entire path like http://localhost:5000/api/todos.
We go back to the Todo directory:
$ cd ..
Then run the command:
$ npm run dev
The app should open and start running on localhost:3000.
In order to be able to access the application from the Internet you have to open TCP port 3000 on EC2 by adding a new Security Group rule. The same way the security group for TCP port 5000 was created. This is done by clicking edit inbound rules.
Creating your React Components
One of the advantages of react is that it makes use of components, which are reusable and also makes code modular.
Our Todo app will have two stateful components and one stateless component.
Go to Todo directory run the command:
$ cd client
Change into the src directory
$ cd src
Inside your src folder create another folder called components
$ mkdir components
Move into the components directory:
$ cd components
Inside components directory create three files Input.js, ListTodo.js and Todo.js.
$ touch Input.js ListTodo.js Todo.js
This creates the three files at the same time.
Open Input.js file
$ vim Input.js
Copy and paste the following:
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
To make use of Axios, which is a Promise based HTTP client for the browser and node.js.
We go into the client directory and run the command:
$ cd ../..
Then run the command:
$ npm install axios
We go to ‘components directory:
$ cd src/components
After that we open the ListTodo.js
$ vim ListTodo.js
In the ListTodo.js copy and paste the following codes:
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
Open the Todo.js file we copy and paste the following code:
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;
We then move into the src directory:
$ cd ..
Open App.js file:
$ vim App.js
Copy and paste the following codes into the file:
import React from 'react';
import Todo from './components/Todo';
import './App.css';
const App = () => {
return (
<div className="App">
<Todo />
</div>
);
}
export default App;
We Exit.
Next we open the App.css file in the src directory and paste the following codes:
.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;
}
}
We Exit.
Still in the same src directory, we open the index.css file:
$ vim index.css
We then copy and paste the following codes:
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;
}
We go to the Todo directory:
$ cd ../..
In the Todo directory, we then run the command:
$ npm run dev
If there are no errors, there should be a message "webfiles compiled successfully".
To view this on the browser, we past the URL:
http://<PublicIP>:3000
Our To-do application is ready and functional with the functionality discussed earlier: creating a task, deleting a task and viewing all your tasks.