https://localhost:8100/add-drug https://localhost:8100/update-drug
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)
Table of Contents
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.
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
const express = require('express');//we installed express using npm previously and we are indicating that it would be used here
const app = express(); //this assigns express to the variable "app" - anything else can be used.
const PORT = process.env.PORT || 3100; //uses either what's in our env or 3100 as our port (you can use any unused port)
app.get('/', function(req, res) {//this listens for a get request for "/" the homepage
res.send('Hello World'); //and sends this response to the console once the request is received
})
app.listen(process.env.PORT || PORT, function() {//specifies port to listen on
console.log('listening on '+ PORT);
console.log('Welcome to the Drug Monitor App at http://localhost:${PORT}');
})
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).
const express = require('express');//we installed express using npm previously and we are indicating that it would be used here
const app = express(); //this assigns express to the variable "app" - anything else can be used.
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
const PORT = process.env.PORT || 3100; //uses either what's in our env or 3100 as our port (you can use any unused port)
app.set('view engine', 'ejs');//Put before app.use, etc. Lets us use EJS for views
//use body-parser to parse requests
app.use(bodyParser.urlencoded({extended:true}));
//indicates which is the folder where static files are served from
app.use(express.static('assets'));
//use morgan to log http requests
app.use(morgan('tiny'));
app.get('/', function(req, res) {//this listens for a get request for "/" the homepage
res.send('Hello World'); //and sends this response to the console once the request is received
})
app.listen(PORT, function() {//specifies port to listen on
console.log('listening on '+ PORT);
console.log(`Welcome to the Drug Monitor App at http://localhost:${PORT}`);
})
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.
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 () => {//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
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;
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)=>{ } // can either retrieve all users from the database or retrieve a single user exports.find = (req,res)=>{ } // edits a user selected using their user ID exports.update = (req,res)=>{ } // deletes a user using their user ID exports.delete = (req,res)=>{ }
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);
Let’s create the CRUD operations next
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 => { res.send(data) //res.redirect('/add-drug'); }) .catch(err =>{ res.status(500).send({//catch error message : err.message || "There was an error while adding the drug" }); });
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 =>{ if(!data){ res.status(404).send({ message : "Can't find drug with id: "+ id}) }else{ res.send(data) } }) .catch(err =>{ res.status(500).send({ message: "Error retrieving drug with id: " + id}) }) }else{ Drugdb.find() .then(drug => { res.send(drug) }) .catch(err => { 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.
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 => { 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 =>{ res.status(500).send({ message : "Error in updating drug information"}) })
const id = req.params.id; Drugdb.findByIdAndDelete(id) .then(data => { 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 =>{ 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.
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 =>{ 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 =>{ 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
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