A basic CRUD MVC app with Node, Express JS, Mongo DB and EJS

Peter posted this on June 20, 2022

Following up on this post, I’ve been using a Google Sheet to calculate the number of drugs I’d need to buy every month for my mum.

I’m a bit free now though & I’ve decided to make it into a web app I can access responsively from anywhere.

Though we use PHP (LAMP stack) more of the time  in building software projects, I’d be building this myself so I’d be using the Javascript environment (love stretching my brain) and putting every step down here (so I can refer to it too :D)

Planning

Before writing any code, what has to be done needs to be understood first of all as that would determine the database structure as well as any routes and code that would be needed.

Information that would be handled are: Name of Drug (text), pills in a card, pills in a pack and how many are taken each day (all numbers).

Operations required would be adding drugs to the database, editing this drugs, viewing the drugs and deleting them from the database when no longer needed.

Starting

This assumes you’ve Node JS installed (I have v16.14.0) and are conversant with Git, HTML, CSS and Javascript.

I’d add a link to the Git Repo so you can fork it if you want to.

https://github.com/pbanigo/drug-monitor

First of all, create your working Folder

From Terminal,

md drug-monitor
cd drug-monitor
touch server.js
git init

This creates the folder I’d be working with, creates an empty server.js file  and initialises the Git environment (for version control)

Next I’d create the package.json file using npm which would hold app information so it can be installed with the same dependencies anywhere it is required.

npm init

After filling all app details, a package.json file would be created and populated with the information.

Next, all dependencies would be installed using npm.

npm install axios body-parser dotenv ejs express mongoose morgan nodemon

I’d comment what each does in the code as we go along

Separation of Concerns/Software Architecture

In order to make it easy to manage and keeping to best practice,  code in production software is usually split with scripts performing the same function bundled together.

The MVC (Model View Controller) method would be used for this to separate the logic and data from presentation/UI.

The folder structure we would create for this is below:

Creating the HTTP Server

First thing to do would be creating the HTTP server using Express.

Copy the following code into your empty server.js and save

  1. const express = require('express');//we installed express using npm previously and we are indicating that it would be used here
  2. const app = express(); //this assigns express to the variable "app" - anything else can be used.
  3. const PORT = process.env.PORT || 3100; //uses either what's in our env or 3100 as our port (you can use any unused port)
  4.  
  5.  
  6. app.get('/', function(req, res) {//this listens for a get request for "/" the homepage
  7.   res.send('Hello World'); //and sends this response to the console once the request is received
  8. })
  9.  
  10.  
  11. app.listen(process.env.PORT || PORT, function() {//specifies port to listen on
  12. 	console.log('listening on '+ PORT);
  13. 	console.log('Welcome to the Drug Monitor App at http://localhost:${PORT}');
  14. })

Return to your terminal and run

npm start

You would see the console logs in the app.listen printed out on your terminal

After this, go to your browser and enter localhost:3100 (or any port you specified)

You should see the response in your app.get printed on the screen.

This is a quick test that everything is working and we can go on to the main building.

Next thing to do would be to create a .env file

touch .env

This file (enabled by dotenv installed earlier) can be used to save your sensitive information (so I can comfortably push the code publicly)

add a PORT variable to the .env file

PORT = 3000

Add the following to the beginning of server.js so it knows it’s going to be using the .env, body-parser and morgan (comments explain what they do)

const bodyParser = require('body-parser');//body-parser makes it easier to deal with request content by making it easier to use
const dotenv = require('dotenv').config();//indicates we would be using .env
const morgan = require('morgan');//this logs requests so you can easily troubleshoot

Add the use statements for these in your server.js (it should be like below once you are done).

  1. const express = require('express');//we installed express using npm previously and we are indicating that it would be used here
  2. const app = express(); //this assigns express to the variable "app" - anything else can be used.
  3. const bodyParser = require('body-parser');//body-parser makes it easier to deal with request content by making it easier to use
  4. const dotenv = require('dotenv').config();//indicates we would be using .env
  5. const morgan = require('morgan');//this logs requests so you can easily troubleshoot
  6. const PORT = process.env.PORT || 3100; //uses either what's in our env or 3100 as our port (you can use any unused port)
  7.  
  8. app.set('view engine', 'ejs');//Put before app.use, etc. Lets us use EJS for views
  9. //use body-parser to parse requests
  10. app.use(bodyParser.urlencoded({extended:true}));
  11. //indicates which is the folder where static files are served from
  12. app.use(express.static('assets'));
  13. //use morgan to log http requests
  14. app.use(morgan('tiny'));
  15.  
  16. app.get('/', function(req, res) {//this listens for a get request for "/" the homepage
  17.   res.send('Hello World'); //and sends this response to the console once the request is received
  18. })
  19.  
  20. app.listen(PORT, function() {//specifies port to listen on
  21. 	console.log('listening on '+ PORT);
  22. 	console.log(`Welcome to the Drug Monitor App at http://localhost:${PORT}`);
  23. })

 

Creating Views (the V in MVC)

Views are the what the end user sees and interacts with.

For this app, the views are going to be rendered using EJS (Embedded Javascript) – you can use pug or any other templating engine of your choice.

Open the ‘views’ folder and create a new file: index.ejs

This can be formatted as a simple HTML file as below:

https://gist.github.com/pbanigo/330d0a365a58ad43c191fc23ce3973a9

To get the server to render this ejs file rather than the text from before, we need to return to server.js and replace

res.send('Hello World');

with

res.render('index.ejs');

Open your chosen port in your browser and confirm that your index.ejs file is being served.

We would now build a simple home page to for the app which would include a button to add new drugs, a table displaying the drugs in the database as well as buttons to edit and delete drugs.

The index.ejs file would be as follows (please note I used fontawesome for the icons and it is referenced in the <head>

https://gist.github.com/pbanigo/92c8fd5738951c61e9805d077f9698d4

If everything is working up to this point, let us split the index.ejs file to make the code more manageable (especially by a team)

in the includes folder (under views), create two files _header.ejs and _footer.ejs

split the code into both files and include them on the index.ejs meaning your files would be as follows:

_header.js

https://gist.github.com/pbanigo/81c6da9ea371febb8bad0524ef214e4d

_footer.js

https://gist.github.com/pbanigo/54f5f1257f3980274ed348464d0b1c1e

index.ejs

https://gist.github.com/pbanigo/05b0aee1d84c7271dc5b53250bbd30c6

Reload your index to confirm everything still works. Styling would be done later.

Now we can create other pages.

First is a form to add new users.

Create a new file, add_drug.ejs in the views folder and copy everything from index.ejs into it.

https://gist.github.com/pbanigo/08687313e65d01e8e1beb8240da9ba4f

Create a new file, update_drug.ejs in the views folder and copy everything from add_drug.ejs into it then edit as below:

https://gist.github.com/pbanigo/c3fc41057a3ce3dcf4432b165f5ec25e

Now we need to add the path to these new forms to the server.js
Add the following after the first app.get in your server.js

app.get('/add-drug', function(req, res) {//this listens for a get request for "/add-drug" from any hyperlink
  res.render('add_drug'); //tells server to respond with add_drug.ejs (.ejs is optional)
})
app.get('/update-drug', function(req, res) {
  res.render('update_drug'); 
})

Visit both pages in your browser to see they load:

https://localhost:8100/add-drug
 
https://localhost:8100/update-drug

If all goes well up to this, we can now move to separating the routes from the main server.js file.

In the routes folder under the server folder, create a new file: routes.js

Add this to the beginning;

const express = require('express');// As in the server.js
const route = express.Router(); //Allows us use express router in this file

Copy the three app.get statements in server.js and replace the app.get with route.get (route we just declared in line 2 of routes.js

route.get('/', function(req, res) {//this listens for a get request for "/" the homepage
  res.render('index.ejs'); //tells server to respond with index.ejs (.ejs is optional)
  //res.render('temp');
})
 
 
route.get('/add-drug', function(req, res) {//this listens for a get request for "/add-drug" from any hyperlink
  res.render('add_drug'); //tells server to respond with add_drug.ejs (.ejs is optional)
})
route.get('/update-drug', function(req, res) {
  res.render('update_drug'); 
})

And at the bottom of the routes.js file, add the following to make it usable elsewhere

module.exports = route;//exports this so it can always be used elsewhere

In server.js, delete the 3 app.get() statements and replace with the following

//load the routes
app.use('/',require('./server/routes/routes'));//Pulls the routes file whenever this is loaded

Check that all routes still work as intended.

Next, we would separate the call back functions into a separate file.
In the services folder under server, create a new file: render.js
In the routes.js file, require this new render file using:

const services = require('../services/render');

Copy the function from the first route.get and add to the render.js as follows:

exports.homeRoutes= function(req, res) {//this listens for a get request for "/" the homepage
 
  res.render('index');
}

This assigns it to the variable, homeRoutes and exports that variable.

Now add this exported variable to the first route.get as follows:

route.get('/', services.homeRoutes);

Since services is required to run this file, the above would simply go into the file indicated and pick out homeRoutes
We can now do the same for the remaining routes
In render.js

exports.addDrug =  function(req, res) {//this listens for a get request for "/add-drug" from any hyperlink
  res.render('add_drug'); //tells server to respond with add_drug.ejs (.ejs is optional)
}
 
exports.updateDrug =  function(req, res) {
  res.render('update_drug'); 
}

In routes.js

route.get('/add-drug', services.addDrug)
route.get('/update-drug', services.updateDrug)

Check if everything still works.

Connecting a Database to the App

We’d be using Mongo DB here as our Database as this would be a simple application.
I’d also be using the Mongo Db Atlas service as I said previously, I’d like the app accessible from anywhere.
You can always create an account at https://mongodb.com and follow their guide to setup if you don’t have an account already https://www.mongodb.com/docs/atlas/getting-started/
Create a Project & Cluster for your App then copy the connection string (for node JS)

Open your .env file
Save the following in it (replace YOUR_CONNECTION_STRING with your actual connection string from Atlas):

MONGO_STR = YOUR_CONNECTION_STRING

Create a new file connect.js in the database folder under server and add the following code

const mongoose = require('mongoose');//mongoose is a JS library that works with MongoDB
 
 
const connectDb = async () =&gt; {//an async function to prevent blocking
    try{
        const conn = await mongoose.connect(process.env.MONGO_STR, {//connect using connection string
        	//outline features that would be used
            useNewUrlParser: true,
            useUnifiedTopology: true,
        })
 
        //Display on console if connection is successful
        console.log(`Database successfully connected at ${conn.connection.host}`);
    }catch(err){//catch errors
        console.log(err);
        process.exit(1);//exit from the process on error
    }
}
 
module.exports = connectDb; //exports the connection function for use anywhere

Require this file in server.js

const connectMongo = require('./server/database/connect');//requires connect.js file

Then call the file (so it executes the connection function) in the body of server.js

//connect to Database
connectMongo();

If all is well up to this point, after Nodemon restarts your server, you should be able to see the following in your terminal:

Database successfully connected at clusterx-xxxxx-00-01.xxxxx.mongodb.net

Creating our Model (the M in MVC)

in the model folder under server, create a new file, model.js
In this file we would write a schema for database entries into the Mongo Database.

Paste the code below in model.js

const mongoose = require('mongoose');
 
let schema = new mongoose.Schema({
    name : {
        type : String,// name would be a string
        required: true,// name is a required property
        unique: true // the value of name must be unique
    },
    card : {
        type: Number, // card would be a number
        required: true
    },
    pack : {
        type: Number,
        required: true
    },
    perDay : {
        type: Number,
        required: true,
        unique: true
    }
})
 
const DrugDB = mongoose.model('all-drugs', schema);
 
module.exports = DrugDB;

Creating our Controllers

In the controller folder under server, create a new file: controller.js
This file would contain the database operations, Create, Read, Update and Delete on our Database.

Add this code to the controller.js file

let Drugdb = require('../model/model');
 
 
// creates and saves a new user
exports.create = (req,res)=&gt;{
 
}
 
 
// can either retrieve all users from the database or retrieve a single user
exports.find = (req,res)=&gt;{
 
}
 
 
// edits a user selected using their user ID
exports.update = (req,res)=&gt;{
 
}
 
 
// deletes a user using their user ID
exports.delete = (req,res)=&gt;{
 
}

Before the exports statement at the end of routes.js, add the following:

// API for CRUD operations
route.post('/api/drugs', controller.create);
route.get('/api/drugs', controller.find);
route.put('/api/drugs/:id', controller.update);
route.delete('/api/drugs/:id', controller.delete);

Coding the CRUD operations

Let’s create the CRUD operations next

CREATE

inside the curly braces on exports.create in the routes.js, add the following code which would ensure an empty request is not being sent and send the data to Mongo if there is information in it.

    // validate incoming request
    if(!req.body){// if content of request (form data) is empty
        res.status(400).send({ message : "Content cannot be emtpy!"});// respond with this
        return;
    }
 
    //create new user
    const drug = new Drugdb({
        name : req.body.name,//take values from form and assign to schema
        card : req.body.card,
        pack: req.body.pack,
        perDay : req.body.perDay
    })
 
    //save created user to database
    drug
        .save(drug)//use the save operation on drug
        .then(data =&gt; {
            res.send(data) 
            //res.redirect('/add-drug');
        })
        .catch(err =&gt;{
            res.status(500).send({//catch error
                message : err.message || "There was an error while adding the drug"
            });
        });

READ

Here there would be two operations, one to pick a drug using it’s ID and another to return all drugs in the database
Add the following code in your exports.find method

    if(req.query.id){//if we are searching for user using their ID
        const id = req.query.id;
 
        Drugdb.findById(id)
            .then(data =&gt;{
                if(!data){
                    res.status(404).send({ message : "Can't find drug with id: "+ id})
                }else{
                    res.send(data)
                }
            })
            .catch(err =&gt;{
                res.status(500).send({ message: "Error retrieving drug with id: " + id})
            })
 
    }else{
        Drugdb.find()
            .then(drug =&gt; {
                res.send(drug)
            })
            .catch(err =&gt; {
                res.status(500).send({ message : err.message || "An error occurred while retriving user information" })
            })
    }

You can test bot methods with POSTMAN to ensure they are working properly before proceeding.
Please ensure your database was connected to successfully.

UPDATE

    if(!req.body){
        return res
            .status(400)
            .send({ message : "Cannot update an empty drug"})
    }
 
    const id = req.params.id;
   	const drugName = req.params.name;
    Drugdb.findByIdAndUpdate(id, req.body, { useFindAndModify: false})
        .then(data =&gt; {
            if(!data){
                res.status(404).send({ message : `Drug with id: ${id} cannot be updated`})
            }else{
                res.send(data);
                //message : `${drugName} was updated successfully!`
            }
        })
        .catch(err =&gt;{
            res.status(500).send({ message : "Error in updating drug information"})
        })

DELETE

    const id = req.params.id;
 
    Drugdb.findByIdAndDelete(id)
        .then(data =&gt; {
            if(!data){
                res.status(404).send({ message : `Cannot Delete drug with id: ${id}. Pls check id`})
            }else{
                res.send({
                    message : `${id} was deleted successfully!`
                })
            }
        })
        .catch(err =&gt;{
            res.status(500).send({
                message: "Could not delete Drug with id=" + id
            });
        });

Now all CRUD operations are working, we can return to making our pages dynamic.

Configuring CRUD operations in the Front End

First of all, we would make the home page display objects from the database when loaded.
Open the render.js file under services
We’d use the Axios HTTP client installed earlier to query our API after which the data received would be parsed and displayed on the front end
require axios in the render.js using:

const axios = require('axios');

In your .env file, add a BASE_URI value

BASE_URI=http://localhost

Note this would be changed when the site is live

change your exports.homeRoutes in render.js to the following:

exports.homeRoutes= function(req, res) {
    // Make a get request to /api/users
    axios.get(`${process.env.BASE_URI}:${process.env.PORT}/api/drugs`)//get request to pull drugs
        .then(function(response){
            res.render('index', { drugs : response.data });// response from API request stored as drugs
        })
        .catch(err =&gt;{
            res.send(err);
        })
}

We would now create a loop in the index.ejs to replace the placeholders with actual dynamic data.
Edit your index.ejs as follows:

https://gist.github.com/pbanigo/469c90be581b2fdb641e4a85f6154d94

In the main.js file add the following:

$("#add_drug").submit(function(event){//on a submit event on the element with id add_drug
    alert($("#name").val() + " sent successfully!");//alert this in the browser
})

Updating the update form
The update form would need to be modified so when it’s called, existing values are already filled in.
Open the render.js and edit exports.updateDrug as follows:

exports.updateDrug =  function(req, res) {
    axios.get(`${BASE_URI}:${PORT}/api/drugs`, { params : { id : req.query.id }})//request a drug from the database using the id
        .then(function(response){
            res.render("update_drug", { drug : response.data})//add drug data when rendering update form
        })
        .catch(err =&gt;{
            res.send(err);
        })
}

Open the update_user.ejs file and here, the form values would be updated with EJS variables so whenever the form is loaded, it shows the information for the current drug.

https://gist.github.com/pbanigo/a531fd6945f3e2e1aa12895298e2bec4

Since we didn’t add an action to the update form, we would use JQuery to specify a custom one in our main.js as below:

$("#update_drug").submit(function(event){// on clicking submit
event.preventDefault();//prevent default submit behaviour
 
//var unindexed_array = $("#update_drug");
var unindexed_array = $(this).serializeArray();//grab data from form
var data = {}
 
$.map(unindexed_array, function(n, i){//assign keys and values from form data
data[n['name']] = n['value']
})
 
var request = {//use a put API request to use data from above to replace what's on database
"url" : `http://${url}/api/drugs/${data.id}`,
"method" : "PUT",
"data" : data
}
 
$.ajax(request).done(function(response){
alert(data.name + " Updated Successfully!");
window.location.href = "/";//redirects to index after alert is closed
})
 
})

Creating, Reading & Updating are now working perfectly in the front end.

What’s left is making the delete buttons work and also styling our app (it looks horrible I know!)

Making the Delete Work

We would also do this with JQuery by calling the delete operation on the API we built

in the main.js, add the following:

if(window.location.pathname == "/"){//since items are listed on homepage
$ondelete = $("table tbody td a.delete"); //select the anchor with class delete
$ondelete.click(function(){//add click event listener
let id = $(this).attr("data-id") // pick the value from the data-id
 
let request = {//save API request in variable
"url" : `http://${url}/api/drugs/${id}`,
"method" : "DELETE"
}
 
if(confirm("Do you really want to delete this drug?")){// bring out confirm box
$.ajax(request).done(function(response){// if confirmed, send API request
alert("Drug deleted Successfully!");//show an alert that it's done
location.reload();//reload the page
})
}
 
})

This completes the operations

STYLING

The Basic operations are complete and can be expanded on in future

Meaning I can see the list of drugs, add to the list and edit and delete drugs if required.

Now I’d need to make it look a little attractive. with CSS.

I’d usually leave design for the designers but since this is being built by me, I’d just make a basic layout that would be usable on mobile and desktop.

First, I’m using this plugin: https://github.com/stazna01/jQuery-rt-Responsive-Tables to ensure the table remains responsive

Then using Bootstrap to add the remaining styles for the form

The current repo can be accessed at:

https://github.com/pbanigo/drug-monitor

Free alternatives to Heroku for hosting your open source projects

Post A Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.