🔖 [Nodejs] CRUD example with MongoDB

2018 - 04 - 04
🔖 [Nodejs] CRUD example with MongoDB
0. [Introduction] After previous notes on Expressjs, we are going deeper into NodeJS + MongoDB by building an example. I'm going to build a simple CRUD project that let me input my favourite heros from Overwatch (CREATE/POST) and show the corresponding statistics for each of them (READ/GET). Once stored in mLab database, I can edit the statistics of each hero (UPDATE)or remove them (DELETE). Prerequisite A project folder should be created, and modules installed, including:
  • nodejs
  • npm init
  • expressjs
  • mongodb
  • body-parser
  • ejs
  • dotenv
  • mLab database
The following steps are all done in project folder. If you haven't installed these modules or don't have a mLab database, you may take a look at:
1. [Initialization] Create an .env file under project folder
DB_SETTING=mongodb://<username>:<password>@ds257808.mlab.com:57808/<database_name>
DB_NAME=<database_name>
Create app.js and import the modules:
//------------------------------------------------------
//dotenv
require('dotenv').config();
//Express
const express = require('express');
const app = express();
//body-parser
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: true}));
//mongoDB
const MongoClient = require('mongodb').MongoClient;
let db, counter;
//ejs
app.set('view engine', 'ejs');
//------------------------------------------------------

//Connect to database
let db;
MongoClient.connect(DB_SETTING, (err, client)=>{
	db = client.db(DB_NAME);
	let port = process.env.PORT || 8000;
	app.listen(port, ()=>console.log(`Listening to port ${port}...`))
});
We connect to the database with MongoClient and db is the variable we're going to use throughout the document.
2. [Read] On the index page, I'm going to show a table that presents current heros from database and a form for me to enter new hero. Since we need to pass and retrieve variable from app.js, we use ejs to build the page. I'm not going to talk about frontend design here so only things related to NodeJS is covered. index.ejs
...
<body>
	<table>
	<%result.forEach(r=>{%>
		<tr>
			<td><%=r.hero%></td>
			<td><%=r.win%></td>
			<td><%=r.on_fire%></td>
			<td><%=r.ed_ratio%></td>
			<td><a href='/edit/<%=r._id%>'><button type='button'>Edit</button></a></td>
		</tr>
	<%})%>
	</table>

	<form method='POST' action='/post'>
		<lable for='hero'>Hero:</lable><input type='text' name='hero'>
		<lable for='win'>Win:</lable><input type='number' name='win'>
		<lable for='on_fire'>On Fire:</lable><input type='number' step='0.01' name='on_fire'>
		<lable for='ed_ratio'>E/D Ratio:</lable><input type='number' step='0.01' name='ed_ratio'>
		<button type='submit'>Submit</button>
	</form>
</body>
Note that we called the parameter result which stores the list of heros, will be passed from app.js to this page. So we iterate it with a loop and present the details of each hero. Each hero is stored in MongoDB with JSON format, for example: Those numbers are stored as string upon posting the form. Besides, we do not have _id assigned to each new hero in the form. _id is a unique identifier for each object in MongoDB, a random slug will be assigned if no _id is declared upon saving. In this practice, we are going to assign _id to each entry using auto increment in app.js so we do not need it in the form. After creating this frontend, we have to tell app.js when to pass the list of heros and render this page. In the database, I have a collection (similar to table in SQL) named heros where I stored all the documents of heros (JSON objects). app.js
//Index
app.get('/', (req, res)=>{
	db.collection('heros').find({}).toArray((err, result)=>{
		if(err){
			console.log(err)
		} else {
			res.render('index.ejs', {result: result})
		}
	})
});
By using toArray() we retrieve the JSON data in an array, which are then passed to a variable named result for the index page.
3. [Create new entry] On the index page, I can enter new hero manually in the form which is then posted to database. This is done by POST method with action link /post as mentioned in the form above. So whenever the form is submitted, a URL request localhost:8000/post is sent. Upon opening this link, we are going to tell NodeJS to post data to database. Since I want to have auto-increment for attribute _id in each hero, we need another collection counter which counts the primary key. Head to mLab and create a new collection named counter, and add a new entry as follow: {_id: 'hero_id', seq: 0} The idea is whenever a new hero is added, the seq number will increase by 1 in this collection. After creating this counter collection, we are going to tell NodeJS how to post new hero to database: app.js
//Add new hero
app.post('/post', (req, res)=>{
	db.collection('counter').findAndModify({_id: 'hero_id'}, {}, {$inc: {seq: 1}}, {new: true}, (err, result)=>{
		if(err){
			console.log(err)
		} else {
			let counter = result.value.seq;
			db.collection('heros').save({
				_id: counter,
				hero: req.body.hero,
				win: req.body.win,
				on_fire: req.body.on_fire,
				ed_ratio: req.body.ed_ratio
			}, (err, result)=>{
				if(err){
					console.log(err)
				} else {
					redirect('/')
				}
			})
		}
	})
});
Upon reaching the link localhost:8000/post, we tell the counter to increase seq by 1 through findAndModify() and $inc. The reason I prefer findAndModify() rather than update() is that we can have return value. The updated seq is saved and assigned to the variable counter. After auto-increment, we save the new hero to database consequently. Note that req.body is used to take the data from the HTML form, thanks to module body-parser. After saving data to database, the page is redirected back to index.
4. [Update] To update hero detail, I use the edit button that I built next to each hero on the index table. Clicking on the button, we will be directed to localhost:8000/edit/<_id of hero>. By rendering this link, I ask NodeJS to search and find matched hero based on the _id. If no result is found a dummy 404 page will popup otherwise we will be redirected to the edit page for such hero. Create a dummy 404.ejs:
<body>
	<h1>No Such Hero :(</h1>
</body>
And an edit.ejs for successful search:
<body>
	<%result.forEach(r=>{%>
	<h1>Update Hero - <%=r.hero%></h1>
	<form method='POST' action='/update'>
		<input type='hidden' name='_id' value='<%=r._id%>'>
		<label for='win'>Win: </label><input type='number' name='win' value='<%=r.win%>'>
		<label for='on_fire'>On Fire: </label><input type='number' step='0.01' name='on_fire' value='<%=r.on_fire%>'>
		<label for='ed_ratio'>E/D Ratio: </label><input type='number' step='0.01' name='ed_ratio' value='<%=r.ed_ratio%>'>
		<button type='submit'>Submit</button>
		<a href='/delete/<%=r._id%>'><button type='button'>Delete</button></a>
	</form>
	<%})%>
</body>
Now, we have to tell NodeJS how to handle this URL request: app.js
//Get a hero
app.get('/edit/:key', (req, res)=>{
	db.collection('heros').find({_id: parseInt(req.params.key)}).toArray((err, result)=>{
		if(err){
			console.log(err)
		} else if(result.length<1) {
			res.render('404.ejs')
		} else {
			res.render('edit.ejs', {result: result})
		}
	})
});
req.params.key refers to :key in URL. This is the dynamic url that we used to search for a particular result. The result is then passed to edit.ejs for editing my statistic of a hero. Once the edit form is submitted, the page is directed to localhost:8000/update. Now we have to tell NodeJS how to update the data in database. app.js
//Update a hero
app.post('/update', (req, res)=>{
	db.collection('heros').updateOne(
		{_id: req.body._id},
		{
			$set:{
				win: req.body.win,
				on_fire: req.body.on_fire,
				ed_ratio: req.body.ed_ratio
			}
		}
	);
	res.redirect('/')
});
Instead of findAndModify(), we use updateOne() as no return is needed. Upon successful update, page is redirected back to index.
5. [Delete] To delete an entry, we use deleteOne() method. In edit.ejs, I have a delete button linking to localhost:8000/delete/<_id of hero>. All we have to do is telling NodeJS to delete an entry from such _id. app.js
//Delete a hero
app.get('/delete/:key', (req, res)=>{
	db.collection('heros').deleteOne({_id: parseInt(req.params.key)}, (err, result)=>{
		if(err){
			console.log(err)
		} else {
			res.redirect('/')
		}
	})
});
To delete an entry, we use GET instead of POST.

Comments

There is no comment yet

New Comment

Please Login to comment