Create a Picture Competition Website in Express JS, MEVN

MEVN stands for Mongo DB, Express JS, Vue JS, and Node JS. In this tutorial, we will teach you how you can create a picture competition web app using Node JS as a back-end server, Mongo DB as a database, and Vue JS as a front-end.

Demo:

What we are going to do:

  1. Setup the Project & User Registration
  2. Login with JWT (Json Web Token)
  3. Logout
  4. Forgot password
  5. Sign-in with Google
  6. Sign-in with Facebook
  7. Create Competition
  8. Show all Competitions
  9. Vote on Competition
  10. AJAX Load More
  11. Competition Detail
  12. Comments on Competition
  13. Delete Competition
  14. Search and Sort
  15. User Notifications
  16. User Profile
  17. Admin Panel

Download Assets

Although you do not need the assets to run this project. But if you want to have a beautiful bootstrap user interface, then you must download the required assets from this Github repo.

Setup the Project

First, you need to download and install Node JS, you can download it from their official site. Then you need to download and install Mongo DB, you can download this from their official site as well. To view the data in Mongo DB you need to download a software called “Mongo DB Compass“, you can download it from here.

Then create a new folder anywhere on your PC and rename it to “picture-competition“. Create a new folder “public” where we will place all our CSS and JS files including Bootstrap and jQuery etc. Create another folder “views” and leave it empty for now, we will place all our HTML files in it.

Node JS

Open a command prompt at the root folder of your project and run the following command:

npm init

And press “enter” for each question asked, it will set the default values. Then run the following command:

npm install express http

This will install the Express framework which we will use for routing and API calls. HTTP module will be used to start the server at a specific port. Then we need to run the following command:

sudo npm install -g nodemon

sudo is used to get admin privileges, nodemon module is used to restart the server automatically if there is any change in the server file. Then create a file named “server.js” and write the following code in it:

var express = require("express");
var app = express();

var http = require("http").createServer(app);

// [set EJS engine]

// [mongo db initialization code goes here]

http.listen(process.env.PORT || 3000, function () {
	console.log("Server started");

	// [mongo db connection goes here]
});

Now run the following command to start the server:

nodemon server.js

If you open the terminal, you will see the message that the server has been started.

User Registration

Login and Registration
Login and Registration

Now we need to display a registration form, when submitted should save the record in Mongo DB. To display HTML pages in Node JS, we need to include a module named “EJS“. So run the following command to install it:

npm install ejs

Make sure to restart the server by running the nodemon server.js command. Then write the following code in your server.js by replacing the [set EJS engine] section:

app.use(express.static(__dirname + "/public"));
app.set("view engine", "ejs");

Now you need to install the Mongo DB module, so run the following command:

npm install mongodb

After that, you need to write the following code in place of the [mongo db initialization code goes here] section:

var mongodb = require("mongodb");
var MongoClient = mongodb.MongoClient;
var ObjectId = mongodb.ObjectId;

After that, replace the section [mongo db connection goes here] with the following code:

MongoClient.connect("mongodb://localhost:27017", function (error, client) {
	if (error) {
		console.error(error);
		return false;
	}

	var db = client.db("picture_competition");
	console.log("Database connected");

	// [routes goes here]

});

Check your command prompt and you will see a message that the database has been connected as well. Now we need to create a route that will be called when accessed from the browser. Write the following code in the [routes goes here] section:

app.get("/", function (request, result) {
	result.render("home");
});

This will render the EJS file, so we need to create a file named “home.ejs” in your “views” folder and write the following code in it:

<%- include ("includes/header") %>

<%- include ("includes/footer") %>

Create a new folder includes inside views folder and create 2 files in it, header.ejs and footer.ejs. Following should be the content of the header.ejs file:

<!DOCTYPE html>
<html>
	<head>
		<title>Picture Competition</title>
		
		<link rel="stylesheet" type="text/css" href="/bootstrap.min.css" />
        <link rel="stylesheet" type="text/css" href="/style.css" />
		<link rel="stylesheet" type="text/css" href="/font-awesome/css/font-awesome.min.css" />

		<script src="/vue.min.js"></script>
		<script src="/vue-resource.js"></script>

		<script src="/jquery-3.3.1.min.js"></script>
		<script src="/bootstrap.min.js"></script>

		<script>
		    Vue.use(VueResource);

		    const mainURL = "http://localhost:3000";
            const accessTokenKey = "accessToken";
            var user = null;
		</script>
	</head>

	<body>

		<nav class="navbar navbar-expand-lg navbar-light bg-light" id="navApp">
			<a class="navbar-brand" v-bind:href="baseUrl">Picture Competition</a>

			<div class="collapse navbar-collapse" id="navbarSupportedContent">
				<ul class="navbar-nav mr-auto">
					<li class="nav-item active">
						<a class="nav-link" v-bind:href="baseUrl">Home</a>
					</li>

                    [search goes here]
				</ul>

				<ul class="navbar-nav">

		            <li v-if="!login" class="nav-item">
		                <a v-bind:href="baseUrl + '/login'" class="nav-link">Login</a>
		            </li>
		 
		            <li v-if="!login" class="nav-item">
		                <a v-bind:href="baseUrl + '/signup'" class="nav-link">Signup</a>
		            </li>
				</ul>
			</div>
		</nav>

And following should be the content of the footer.ejs:

	</body>
</html>

We will be creating multiple Vue JS apps, so right now we will create a Vue JS app for this navigation bar. Write the following lines in your footer.ejs:

<script>
	var navApp = new Vue({
		el: "#navApp",
		data: {
			baseUrl: mainURL,
			login: false,
		}
	});
</script>

Access your project from the browser from this URL: http://localhost:3000/ and you will see the login and signup button at the top navigation bar. Because right now the user is not logged in. Click on the signup link and you will be redirected to the “404 not found” page.

Show Signup Form

Now we need to create that route in our server.js:

app.route("/signup")
    .get(function (request, result) {
        result.render("signup");
    });

Then create a new file named signup.ejs inside the views folder. We will be using AJAX many times in this project, so we will place it in a separate file so we can re-use it later easily.

Create a file named app.js inside the public folder and write the following code in it:

var myApp = {
	callAjax: function (url, formData, callBack = null) {
		var ajax = new XMLHttpRequest();
		ajax.open("POST", url, true);

		ajax.onreadystatechange = function () {
			if (this.readyState == 4) {
				if (this.status == 200) {
					if (callBack != null) {
						callBack(this.responseText);
					}
				}

				if (this.status == 500) {
					console.log(this.responseText);
				}
			}
		};

		ajax.send(formData);
	}
};

Then in your header.ejs you need to include that file as Javascript.

<script src="/app.js"></script>

We need to display an alert message when the user is signed up or when there is an error during sign-up. We will be using the Sweetalert library to display alerts. Include that in your header.ejs:

<script src="/sweetalert.min.js"></script>

Finally, we will write the following code in our signup.ejs:

<%- include ("includes/header") %>

<div class="container margin-container" id="registerApp">
	<div class="row">
		<div class="offset-md-3 col-md-6">

			<h2 class="text-center">Register</h2>

			<form method="POST" v-bind:action="baseUrl + '/signup'" v-on:submit.prevent="doRegister">
				<div class="form-group">
					<label>Name</label>
					<input type="text" name="name" class="form-control" required />
				</div>

				<div class="form-group">
					<label>Email</label>
					<input type="email" name="email" class="form-control" required />
				</div>

				<div class="form-group">
					<label>Password</label>
					<input type="password" name="password" class="form-control" required />
				</div>

				<input type="submit" name="submit" value="Register" class="btn btn-primary" />
			</form>
		</div>
	</div>
</div>

<script>
	var registerApp = new Vue({
		el: "#registerApp",
		data: {
			baseUrl: mainURL
		},
		methods: {
			doRegister: async function () {
				const form = event.target;

				// disable the submit button and show "Loading..." text
		        form.submit.setAttribute("disabled", "disabled");
		        form.submit.value = "Loading...";

				var formData = new FormData(form);
				myApp.callAjax(form.getAttribute("action"), formData, function (response) {
	                // convert the JSON string into Javascript object
	                var response = JSON.parse(response);

	                // enable the submit button
	                form.submit.removeAttribute("disabled");
	                form.submit.value = "Register";

	                // if the user is created, then redirect to login
	                if (response.status == "success") {
	                	swal("Registered", response.message, "success");
	                } else {
	                	swal("Error", response.message, "error");
	                }
				});
			}
		}
	});
</script>

<%- include ("includes/footer") %>

Refresh the page and you will see a signup form. But nothing happens when you click the submit button, it is because we need to create an API for signup.

Saving the Data in Mongo DB

We will be sending AJAX requests with a form data object in this project. To parse that form data object on the server-side, we need to install a module named express-formidable.

And we need to encrypt plain text password fields into a hashed string before saving them in Mongo DB. So we need to install a module named bcrypt.

After the user successfully registered, we need to send a verification link to his email address to verify the account. He will only be able to log in if the account is verified. To send an email we will be using a module named nodemailer.

Note: Please enable the “less secure apps” option from your google account from this page.

Then open your command prompt and run the following commands at the root of your project folder to install the above-discussed modules:

npm install express-formidable bcrypt nodemailer

Then open your server.js file and include the above-installed module at the top:

var expressFormidable = require("express-formidable");
app.use(expressFormidable());

const mainURL = "http://localhost:3000";

const nodemailer = require("nodemailer");
var nodemailerFrom = "your_email@gmail.com";
var nodemailerObject = {
	service: "gmail",
	host: 'smtp.gmail.com',
    port: 465,
    secure: true,
	auth: {
		user: "your_email@gmail.com",
		pass: "your_password"
	}
};

var bcrypt = require("bcrypt");

Enter your correct email and password of your Google account of whom you have enabled “less secure apps”. Then create a post route for “signup” by chaining it with GET route we created earlier.

// route for signup requests
app.route("/signup")
 
    // get request accessed from browser
    .get(function (request, result) {
        // render signup.ejs file inside "views" folder
        result.render("signup");
    })
    // post request called from AJAX
    .post(async function (request, result) {
 
        // get values from signup form
        var name = request.fields.name;
        var email = request.fields.email;
        var password = request.fields.password;
		var verificationToken = new Date().getTime();
		var createdAt = new Date().getTime();
 
        // check if email already exists
        var user = await db.collection("users").findOne({
            "email": email
        });
 
        if (user != null) {
            result.json({
                "status": "error",
                "message": "Email already exists."
            });
            return true;
        }
 
        // encrypt the password
        bcrypt.hash(password, 10, async function (error, hash) {
 
            // insert in database
            await db.collection("users").insertOne({
                "name": name,
                "email": email,
                "password": hash,
                "picture": "",
                "accessToken": "",
                "notifications": [],
                "bio": "",
                "dob": "",
                "country": "",
                "phone": "",
                "website": "",
                "twitter": "",
                "facebook": "",
                "googlePlus": "",
                "linkedIn": "",
                "instagram": "",
                "resetToken": "",
                "isVerified": false,
                "verificationToken": verificationToken,
                "createdAt": createdAt
            });

            var transporter = nodemailer.createTransport(nodemailerObject);

			var html = "Please verify your account by click the following link: <br><br> <a href='" + mainURL + "/verifyEmail/" + email + "/" + verificationToken + "'>Confirm Email</a> <br><br> Thank you.";

			transporter.sendMail({
				from: nodemailerFrom,
				to: email,
				subject: "Email Verification",
				text: html,
				html: html
			}, function (error, info) {
				if (error) {
					console.error(error);
				} else {
					console.log("Email sent: " + info.response);
				}
				
				// send the response back to client
	            result.json({
	                "status": "success",
	                "message": "Signed up successfully. Kindly check your email to verify your account."
	            });
			});
        });
    });

Refresh the browser and try to signup. Fill the signup form and hit submit. You will see a Sweetalert that an account has been created and you will also receive an email of account confirmation.

Email Verification

Email Verification
Email Verification

Now we need to create a route to verify the email address because without this, the user will not be able to log in. So we just need to create a GET route in our server.js:

app.get("/verifyEmail/:email/:verificationToken", async function (request, result) {
	const email = request.params.email;
	const verificationToken = request.params.verificationToken;

	var user = await db.collection("users").findOne({
		$and: [{
			"email": email,
		}, {
			"verificationToken": parseInt(verificationToken)
		}]
	});

	if (user == null) {
		result.render("verify-email", {
			"status": "error",
			"message": "Email does not exists. Or verification link is expired."
		});

		return false;
	}

	await db.collection("users").findOneAndUpdate({
		$and: [{
			"email": email,
		}, {
			"verificationToken": parseInt(verificationToken)
		}]
	}, {
		$set: {
			"verificationToken": "",
			"isVerified": true
		}
	});

	result.render("verify-email", {
		"status": "success",
		"message": "Account has been verified. Please try login."
	});
});

If the account is confirmed successfully or if there is an error in the confirmation like we will add the expiry time to the link. So the user has to resend a verification email if a certain time is passed. In either case, we need to create a new file verify-email.ejs inside your views folder to display a success or an error message.

<%- include ("includes/header") %>

<div class="container margin-container">
	<div class="row">
		<div class="offset-md-3 col-md-6">

			<h2 class="text-center" style="margin-bottom: 20px;">Email Verification</h2>

			<% if (status == "success") { %>
				<div class="alert alert-success"><%= message %></div>
			<% } %>

			<% if (status == "error") { %>
				<div class="alert alert-danger"><%= message %></div>
			<% } %>
		</div>
	</div>
</div>

<%- include ("includes/footer") %>

Click on the link you receive in your provided email address, you will see a success message using a Bootstrap alert if the account is confirmed successfully.