Final Project (Node) for Intermediate Web Programming

Setting Up The Project

Create a folder named node-final-project. Do not put this folder inside the int-web-dev folder, we want to keep it separate so that it can have it's own separate Git repository.

Open the folder in VSCode.

Add a README.md file to the project folder.

Add a heading to the readme file that says Intermediate Web Final Project.

Add a .gitignore file to the project folder and put this in it:

node_modules

Creating a Git Repository

Open the terminal in VS Code, cd into the project folder (if you are not already there) and initialize a Git repository:

git init

Add and commit the readme file, the commit message can be Initial Commit

Publish the repository to GitHub.

Run this command (from the project folder) to initialize it as an NPM project:

npm init -y

Install Express

Express is a package that allows you to build web servers with JavaScript code.

Install the Express package:

npm install express

Create a file named app.js in the project folder and put this code in it:

const port = 8080; // We'll run the server on port 8080

// IMPORTS
const express = require('express');
const app = express();

// MIDDLEWARE


// ROUTES
app.get('/', (req, res) => {
   res.send('<h1>Hello World from Express!</h1>');
});

// START THE SERVER
const server = app.listen(port, () => {
   console.log("Waiting for requests on port %s", port);
});

Now run the app (server) by entering this command:

node app.js

Now open a browser tab and navigate to http://localhost:8080

To stop the server, click in the terminal and then press ctrl + c.

Add Static Pages and Files

Create a folder named public (put it in the project folder).

Inside the public folder, add a file named static-page.html and put this code inside of it:

<!DOCTYPE html> 
<html lang="en"> 
	<head> 
		<title>Static Page</title>
	  <meta charset="utf-8"> 
	  <meta name="viewport" content="width=device-width, initial-scale=1" /> 
	</head>
	<body>
		<h1>This is a static page</h1>
	</body>
</html>

Now we'll configure the server to use the 'public' folder as a static one (in NodeJS it's common to use a 'public' folder for your static files). Add this code to app.js, put it underneath the comment that says MIDDLEWARE:

app.use(express.static('public'));

Now start the server by entering node app.js (if the server is already started, then stop it with ctrl + c), then open this URL in the browser: http://localhost:8080/static-page.html

The public folder will contain our 'static' files for our website. Static web pages are ones that that don't change. When a request comes in for a static file, the server will simply send the file, as is, to the browser.

We'll also put other files in it, such as images, .js, and .css files. Go ahead and create these 3 folders inside the public folder:

  1. images
  2. css
  3. js

Then put an image in the images folder (any old jpg or png will do).

In the js folder, add a file named main.js and put this code in it:

console.log("This is main.js...");

In the css folder, add a file named main.css and put this code in it:

h1{ color: midnightblue; }

Now update the static-page.html to look like this (note that you'll have to use the proper name for your image file by setting the SRC attribute of the IMG element):

<!DOCTYPE html> 
<html lang="en"> 
	<head> 
		<title>Static Page</title>
	  <meta charset="utf-8"> 
	  <meta name="viewport" content="width=device-width, initial-scale=1" /> 
		<link rel="stylesheet" type="text/css" href="/css/main.css">
		<script src="/js/main.js"></script>
	</head>
	<body>
		<h1>This is a static page</h1>
		<img src="/images/YOUR-IMAGE-NAME-GOES HERE"> <!--set the path to YOUR image file-->
	</body>
</html>

Note that the links to the static files start with a forward slash. These paths are known as doc root relative links.

Stop the server (ctr + c). Then restart it. Now visit the static page in your browser.

Add Dynamic Pages

In addition to static files, we can also use 'dynamic' pages. These are pages that allow us to run some code on the server before sending the response to the browser.

Here's an example, add this code to the ROUTES section of app.js:

app.get("/dynamic-page.html", (req, res) => {
   const currentTime = new Date();
   res.send(`<h1>The current time is ${currentTime.toString()}</h1>`);
});

Stop the server (ctrl + c) and then start it again. Then visit this page in the browser: http://localhost:8080/dynamic-page.html

Dynamic pages allow you to do some very powerful things with your website! Unlike a static page, which sends the HTML/CSS/JS code to the browser to be executed, a dynamic page can run code on the server before sending the HTML/CSS/JS code to the client. In the code sample above, the server is simply computing the current time and embedding it into the HTML code that gets sent to the browser. You could do much more complicated and useful things by running code on the server. For example, you run code on the server that connects to a database and embeds the results of a SQL query in the response before sending it to the client

It takes a bit of a mental shift when you start working with dynamic websites. You might think that for every URL on the site, there is a page (.html file) that goes with it. But this is not the case for dynamice sites, as you can see from the previous example, there is no file named dynamic-page.html in the project. Instead we defined a route for the url /dynamic-page.html. So when the browser makes a request for http://localhost:8080/dynamic-page.html, the server responds by executing the code defined in the route. From the browser's perspective, it appears that there is a file named dynamic-page.html on the server.

Instead of thinking about the pages/files on a site, you should think about the routes (URLs) that your server will respond to.

Nodemon

It get's to be a little tedious when you have stop and start the server every time you make a change. Luckily there's an NPM package that we can use so that the server will automatically restart whenever we save changes.

Run this command (from the project folder) to install the Nodemon package:

npm install nodemon --save-dev

Now stop the server (if it's running) and we'll start it using Nodemon by entering this command (go ahead and enter the command in the terminal):

npx nodemon app.js

Now, whenever you save changes, Nodemon will automatically restart the server and reload the browser for you.

Using Dynamic Pages with Templates

Now we'll use templates to make it easier to manage our pages. There are lots of templating packages that you can use for this, but we'll be using EJS (which stands for embedded javascript) templates.

Install the EJS package:

npm install ejs

Create a folder named views in the project folder.

In the views folder, create a file named default-layout.ejs and put this code in it:

<!DOCTYPE html> 
<html lang="en"> 
	<head> 
		<title><%= title %></title>
	  <meta charset="utf-8"> 
	  <meta name="viewport" content="width=device-width, initial-scale=1" /> 
		<link rel="stylesheet" type="text/css" href="/css/main.css">
		<script src="/js/main.js"></script>
	</head>
	<body>
		<header>
			<h1>Header</h1>
		</header>
		<nav>
			<ul>
        <li><a href="/">Home</a></li>
        <li><a href="/blog">Blog</a></li>
        <li><a href="/contact-me">Contact</a></li>
      </ul>
		</nav>
		<div id="content">
			<main>
				<%- content %>
			</main>
		</div>
		<footer>
			Footer
		</footer>
	</body>
</html>

This is a template that can be used for dynamic pages. Note that there are placeholders (variables) in the title and main elements. We'll inject content into these place holders when a page is requested.

Now add this to the MIDDLEWARE section in app.js:

app.set('view engine', 'ejs')

This sets up our app to use EJS templates (as mentioned, there are many other templating packages available, so we need to tell our app that we're using EJS).

By default, the app will look for .ejs files in the views folder.

Now let's update the route to our home page to look like this:

app.get('/', (req, res) => {
   res.render('default-layout', {
      title: "My Home Page",
      content: "<h1>This is my home page</h1>"
   });
});

Now run the app (npx nodemon app.js) and open the home page in the browser (localhost:8080). You should see the default layout template with the title and content injected from the route code. Note that the render() method is called on the 'response' object in order to render a template. The first parameter passed into render() is the name of the template to use (without the .ejs file extension). The second parameter is an object with properties that match the placeholders in the template.

Now we'll split the default-layout into a few different template files so that we can re-use the files for other pages. These files are called 'partials'.

Inside the views folder, create a folder named partials.

Create a file named top.ejs inside the partials folder, and put this code into it:

<!DOCTYPE html> 
<html lang="en"> 
	<head> 
		<title><%= title %></title>
	  <meta charset="utf-8"> 
	  <meta name="viewport" content="width=device-width, initial-scale=1" /> 
		<link rel="stylesheet" type="text/css" href="/css/main.css">
		<script src="/js/main.js"></script>
	</head>
	<body>
		<header>
			<h1>Header</h1>
		</header>
		<nav>
			<ul>
	        <li><a href="/">Home</a></li>
	        <li><a href="/blog">Blog</a></li>
	        <li><a href="/contact-me">Contact</a></li>
	      </ul>
		</nav>
		<div id="content">
			<main>

Create a file named bottom.ejs inside the partials folder, and put this code into it:

         </main>
		</div>
		<footer>
			Footer
		</footer>
	</body>
</html>

Now we can re-use these 'partial' templates to create templates for the pages we create, let's do that now.

Update the default-layout.ejs to look like this:

<%- include('partials/top') %>
<%- content %>
<%- include('partials/bottom') %>

Notice how this version of the default-layout template file 'includes' the top and bottom partial templates. Now we can re-use the partial files in other templates.

In the views folder, create a file named home.ejs and put this code into it:

<%- include('partials/top') %>
<h1>Welcome to My Website</h1>
<p>This page will soon show a list of links to blog posts that I have written (in markdown files)</p>
<%- include('partials/bottom') %>

Now, in app.js we need to update the route for the home page to look like this:

app.get('/', (req, res) => {
  res.render('home', {
     title: "My Home Page"
  });
});

Now, run the server (if it's not already running) and then navigate to localhost:8080 in the browser. You should see the 'home' template, which includes the 'top' and 'bottom' partial templates.

Now we'll create another page template for the site's 'contact' page. Create a file in the views folder named contact.ejs and put this code in it:

<%- include('partials/top') %>
<h1>Contact Me</h1>
<form method="POST" action="/contact-me">
    <label>First Name:</label>
    <br>
    <input type="text" id="txtFirstName" name="firstName">
    <br>

    <label>Last Name:</label>
    <br>
    <input type="text" id="txtLastName" name="lastName">
    <br>

    <label>Email:</label>
    <br>
    <input type="text" id="txtEmail" name="email">
    <br>  

    <label>Comments:</label>
    <br>
    <textarea id="txtComments" name="comments"></textarea>
    <br>
    <input type="submit" value="SUBMIT">   
</form>
<%- include('partials/bottom') %>

Notice how the action attribute is set to /contact-me/submit on the form element, we'll discuss that a bit more in a minute. But first, let's define a route for the contact page.

Add this route to app.js, add it to the ROUTES section:

app.get('/contact-me', (req, res) => {
  res.render('contact', {
     title: "Contact Me"
  });
});

Run the app, and visit this URL in your browser localhost:8080/contact-me. You should see the contact page.

If you submit the contact form now, you'll see an error message saying 'Cannot POST /contact-me'. To fix this, we need to add a route to app.js that handles POST requests to the /contact-me route.

Add this route to app.js:

app.post('/contact-me', (req, res) => {
  res.send("<h1>TODO: Handle contact-me form posts</h1>");
});

Note that this route invokes the post() method on the app object, which means that it will handle POST requests. When a form is submitted, the browser will send the data to it's destination as a POST request. We'll finish this route later. But for now, we can take it a step further.

In order get the data that was entered into the form, we need to install a package named body-parser. Run this command from the terminal to install it:

npm install body-parser

Now import it by adding this to the IMPORTS section in app.js:

const bodyParser = require('body-parser');

This package/module will allow the app to get the data entered into a form by adding a 'body' property to the request object.

Now add this code to the MIDDLEWARE section in app.js (it tells the app to 'use' the body parser package):

// allow the app to receive data from form submits
app.use(bodyParser.urlencoded({ extended: true }));

Update the POST route for /contact-me (in app.js) to look like this:

app.post('/contact-me', (req, res) => {
  res.send("<h1>TODO: Handle contact-me form posts</h1>" + JSON.stringify(req.body));
});

Because of the bodyParser module, we can now access a 'body' property of the request object (the req parameter). Now, if you submit the form from your browser, you should see the data you entered.

As mentioned, we'll finish the route later by adding code that sends an email to you whenever someone submits the form.

Converting Markdown Files into HTML

Markdown is an excellent format for keeping notes because of the minimal syntax.

For this web application, we'll write our blog posts in markdown and then convert the markdown files into HTML pages. We'll use a few NPM packages to do this.

Add a folder named blog to your project folder.

Then put a sample blog file in it, named sample-blog-page.md, and paste this markdown code into it:

---
title: "This is just a sample blog post"
author: "John Doe"
description: "A nice description of this post"
published: "2024-3-1"
tags: ["sample"]
---
# One hash tag for an H1 affect

## This is an H2 (two hash tags)

### This is an H3 (3 hash tags)

# Use Ctrl+Shift+V to preview a markdown file in VSCode

this is **bold** text

This is a link [this is the link text](https://www.google.com)

### Here's a JavaScript code sample:
```js
if(this){

}
```
The above code is a JavaScript sample.

## Lists
### Ordered Lists
1. Item 1
1. Item 2
1. Item 3

### Unordered Lists
- Item 1
- Item 2
- Item 3

## Here's how you can put images in markdown:
![This is an eagle](/images/eagle.png)

Note that you'll have to update last line so that it displays the image that you have in your images folder.

Also note that at the top of the file, in between the triple dashes, we can add metadata about the blog post. This is known as front matter and it's similar to the head element of an HTML file. All of your markdown files should include front matter that has a title and author property. The published property can be omitted, but then (as you'll see), it will not get converted to a web page. This allows you to keep blog pages off your public website until they are ready to be published.

In order to convert markdown files to HTML, we'll use the exact same module that you created in the node sample project. But first, you need to install the gray-matter and markdown-it packages/modules by running this command in the terminal (you can install both packages with a single command):

npm install gray-matter markdown-it

Now create a folder named modules inside the project folder.

Create a file named markdown-helpers.js inside the modules folder and put this code into it:

const fs = require('fs');
const matter = require('gray-matter'); // converts md file (with gray matter) into an object
const md = require("markdown-it")({html:true});// html:true allows you to put HTML tags in the markdown files

/**
 * Converts a markdown file into an object that includes properties
 * for the file's front matter, a 'content' property which consists
 * of the files' markdown code, and an 'html' content which consists
 * of the markdown code that has been converted to HTML
 * 
 * @param {string} filePath   The path to the markdown file
 * @returns {object}
 */
function convertMarkdown(filePath){
  const obj = matter.read(filePath);
  if(obj){
      obj.html = md.render(obj.content);   
  }
  return obj;
}

/**
 * Takes a path to a markdown file, reads the front-maktter (metadata) 
 * and returns an object that that includes the title, author, published
 * date of the file. 
 * @param {string} filePath   The path to the markdown file 
 * @returns {object}          An object that has a title, author, and published property
 */
function getMarkdownMetaData(filePath){
  const obj = matter.read(filePath);
  const metaData = {
      title: obj.data.title || "No Title",
      author: obj.data.author || "No Author",
      published: obj.data.published || false
  }
  // add the object to the blogList array
  return metaData;
}

/**
 * TODO: add documentation comments here
 */
function getBlogList(folderPath){
  const blogFiles = fs.readdirSync(folderPath); 
  const blogInfoList = [];
  blogFiles.forEach(fileName => {
    if(fileName.endsWith(".md")){
      const blogInfo = getMarkdownMetaData(folderPath + fileName);
      if(blogInfo.published){
        blogInfo.link = "/blog/" + fileName.replace(".md","")
        blogInfoList.push(blogInfo)
      }
    }
  });
  return blogInfoList;
}

exports.convertMarkdown = convertMarkdown;
exports.getMarkdownMetaData = getMarkdownMetaData;
exports.getBlogList = getBlogList;

This is the same module that we created in our node sample project, BUT I've added a function named getBlogList(). This function uses the Node fs modules to get all the .md files within a specified folder. It then loops through the files and creates 'meta data' object for each one. It then adds each of these objects to an array, and then returns the array.

We'll use this array to generate a list of links to all the blog pages that are 'published' to our website.

There's a lot going on in this function. So we need to take the time to step through it with the debugger.

Note that the function makes use of the getMarkdownMetaData() function to get a 'meta data' object for each .md file. It then checks to see if the meta data published property is truthy. If so, the object is added to the array that the function returns. If not, the object is omitted from the returned array. This mechanism allows you to have markdown files on your site that you are still working on.

Create a blog list page

Now add this code to the ROUTES section of app.js:

const {getBlogList, convertMarkdown} = require("./modules/markdown-helpers")
const pathToBlogFolder = __dirname + '/blog/'; 

app.get('/blog', (req, res)=>{
  const blogList = getBlogList(pathToBlogFolder);
  res.send(JSON.stringify(blogList));
})

If you run the app, and then navigate to localhost:8080/blog you should see an array that has one object in it. The object should include a title, author, published, and link property.

Note that I've imported the getBlogList() and convertMarkdown() functions from our markdown-helpers modules. We aren't using convertMarkdown() yet, but we will be later.

Now we are ready to start working on the page that lists all of your 'published' blog posts. Create a file in the views folder named blog-list.ejs and put this code into it:

<%- include('partials/top') %>
  <h1>Blog Posts</h1>
  <ul>
  <% for (let i = 0; i < posts.length; i++) { %>
      <li>
         <a href="<%=posts[i].link%>"><%=posts[i].title%></a>
      </li>
  <% } %>
  </ul>
<%- include('partials/bottom') %>

The syntax is a little stange, but you can embed JavaScript code in between the angle bracket and percent characters, which is where a for loop is set up. The loop will iterate through an array of 'post' objects that we'll pass through from the /blog route. This array will be the same one that is returned by the getBlogList() function.

Now update the /blog route in app.js to look like this:

app.get('/blog', (req, res)=>{
  const blogList = getBlogList(pathToBlogFolder);
  res.render('blog-list', {
    title: "Blog",
    posts: blogList
  });
})

Run the app, and navigate to localhost:8080/blog. You should see a list that includes one hyperlink (we only have one .md file in the blog folder, but if you add more, they should show up in the list too).

The link doesn't work yet, but we'll work on that route next.

We could optimize the code a bit by declaring and initializing the blogList constant before the route. This would cause the getBlogList() function to get invoked only once, when we start the server, rather than each time a request comes in for the route.

The blogList array will be stored in RAM as long as the app is running. This won't be a problem unless you have many, many elements in the array. It's much faster for the application to get the array from RAM rather than it is to re-run the getBlogList() each time you need to 'render' the /blog route.

If you want to do this optimization, then the final code for the route will look like this:

const {getBlogList, convertMarkdown} = require("./modules/markdown-helpers")
const pathToBlogFolder = __dirname + '/blog/'; 
const blogList = getBlogList(pathToBlogFolder);

app.get('/blog', (req, res)=>{
  res.render('blog-list', {
    title: "Blog",
    posts: blogList
  });
});

Note that with this approach, if you add a new markdown file to the blog folder, you'll have to restart the app in order for it to be generate the updated blogList array.

Create a blog page (a route with a parameter in it)

Express has a really useful feature that allows routes to declare parameters. This might sound strange at first, but it will make sense when you see it in action.

Add this route to app.js:

app.get("/blog/:post", (req, res) => {
   res.send("The <b>:post</b> param is set to: " + req.params.post)
});

Note that the route includes a colon (the first parameter that we are passing into the get() method of the app). This means that we are declaring a parameter named post.

Now start the server and navigate to these URLs and observe the response that is sent to the browser:

  1. http://localhost:8080/blog/foo
  2. http://localhost:8080/blog/bar
  3. http://localhost:8080/blog/sample-blog-page

When we finish the code for the route, the last URL in the list will display the sample-blog-page markdown file (after converting it to HTML).

Update the route to look like this:

app.get("/blog/:post", (req, res) => {
  try{
    const pathToFile = pathToBlogFolder + req.params.post + ".md";
    console.log("Markdown file: " + pathToFile);
    const obj = convertMarkdown(pathToFile);
    res.render('default-layout', {
       title: obj.data.title,
       content: obj.html
    });
  }catch(e){
    console.log(e);
    res.status(404).redirect("/404");
  }
});

Now navigate to this URL in your browser: http://localhost:8080/blog/sample-blog-page and you should see your first blog page!!!

Notice that we used a try/catch block in case there is no markdown file that matches the route :post parameter. This would cause our program to 'throw' an error and we can prevent it from crashing by 'catching' it. To see how this works, navigate to this URL: http://localhost:8080/blog/foo. In this case we send a status code of 404 and redirect to a 404 page that we have not yet created. We'll do that next.

Page Not Found (404 page)

Let's now work on a 'page not found' page. Add this route to app.js:

app.get("/404", (req, res) => {
  res.status(404);
  res.render('default-layout', {
     title: "Page Not Found",
     content: "<h1>Sorry!</h1><h3>We can't find the page you're requesting.</h3>"
  });
});

Now navigate to the route in the browser (localhost:8080/404), and you should see the 'page not found page'

If you navigate to this URL: http://localhost:8080/some-page/ you'll see the the server responds with a message that says 'Cannot GET /some-page/'. This is because there we have not defined a route that matches the request.

We can add a 'wildcard' route that will match any request and redirect to the 404 page, but we need to make sure that it comes after all of the other routes in app.js.

Add this after all the other routes in app.js:

app.all('*', (req, res) => {
  res.status(404).redirect("/404");
});

Note that we are calling the all() method of the application object, which means that the route will respond to all types of requests (GET, POST, etc.). Also note that the first parameter is an asterisk, which is known as the 'wildcard' character, which means that it will match any requested route (this is why we need to make sure that this route is the last one, because we need to let the other routes have a chance to match the incoming request before this one gets used). Now navigate to the bogus URL again ( http://localhost:8080/some-page/ ), and you should see that you get redirected to the 'page not found' page.

Installing Prism

We'll use Prism JS to display the code samples in our blog pages.

Go to this page to configure and download the Prism javascript and css files.

  1. Choose a theme
  2. Choose the languages that you'll be using for your code samples (you should leave the default ones selected, but you may want to add additional ones).
  3. Make sure to include the Unescaped Markup and Normalize Whitespace plugins.
  4. At the bottom of the page, you should see links to download both the .js and .css files after you've made your configuration settings.
  5. Create a folder named prism inside the public folder and put the downloaded files in this folder.
  6. Add the following HTML code to the top.ejs template file (put it inside the HEAD element):
<link rel="stylesheet" href="/prism/prism.css">
<script src="/prism/prism.js"></script>

Finally, check out the sample blog page (http://localhost:8080/blog/sample-blog-page/) and note that the code JavaScript code sample is using Prism.

Deploy the web application to your live server

Before moving on, we should try deploying the app to your Western Hosting account. We'll discuss this in class.

Finishing the Contact page

Currently, when you submit the contact form in the browser, the server will respond with a 'TODO' message. Now we'll begin to replace that message with code that sends you an email. The email sent by the app will notify you that someone has submitted the form, and it will include the data that was entered into the contact form.

We'll take this in small steps, and test the code as we progress. Start out by updating the route that handles POST requests to /contact-me so that it looks like this:

app.post('/contact-me', (req, res) => {
  // res.send("<h1>TODO: Handle contact-me form posts</h1>" + JSON.stringify(req.body));
  
  // Destructure the req.body object into variables
  const {firstName, lastName, email, comments} = req.body;
  
  // Make sure none of the variables are empty (falsy)
  if(firstName && lastName && email && comments){
    // TODO: make sure the email entered into the form is a valid email address
    // TODO: make sure the form does not include spam
    // TODO: send an email to YOUR email address with the data entered into the form
    res.send("TODO: Validate the email address entered, make sure the form does not include SPAM, then send an email to my email account")
  }else{
    res.status(400).send("Invalid request - data is not valid")
  }

});

Notice that the code for the route destructures the body of the request (the data that was 'posted' from the form) into individual variables.

Then the IF statement checks those variables to make sure none contain an empty string (which would evaluate to 'falsy').

If any of the variables are falsy, then we end up inside the ELSE block, and send a 400 status code in the response, which indicates that the request is invalid. We also send a message which will notify the user that their POST request is not valid.

In order to take care of the 3 TODO items, we'll create a module.

Create a file in the modules folder named contact-helpers.js, and put this code into it:

// validates an email address (returns true it is valid, false if it is not)
function isValidEmailAddress(email){
  // a regular expression that checks if a string matches the pattern of an email address.
  var regExp = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
  return regExp.test(email);
}

// checks a string to see if a URL is in it (returns true if the string has a URL in it, false if not)
// (if there is a URL in the form data, then we consider it to be SPAM!)
function containsURL(str){
  // a regular expression that checks if a string contains a URL
  var regExp = /\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i;
  return regExp.test(str);
}

function isValidContactFormSubmit(firstName, lastName, email, comments){
  // make sure none of the params are empty
  if(firstName && lastName && email && comments){
    // make sure no SPAM has been included in the form
    if(containsURL(firstName) || containsURL(lastName) || containsURL(comments)){
      return false;
    }
    // make sure the email address is valid
    if(!isValidEmailAddress(email)){
      return false;
    }
    // if everything passes validation, then return true
    return true;
  }
  
  return false;
  
}

function sendEmailNotification(message, callback){
  // TODO: we'll work on this function later
}

exports.isValidContactFormSubmit = isValidContactFormSubmit;
exports.sendEmailNotification = sendEmailNotification;

Here are some things to note about the code for the contact-helpers module:

  1. The isValidEmailAddress() function returns true if its parameter is a valid email address (it's using a regular expression to check the parameter). Otherwise it returns false.
  2. The containsURL() function uses a regular expression to see if the parameter contains a URL
  3. The isValidContactFormSubmit() function checks all the parameters and makes use of the two other functions. It will return true if all the parameters are valid, and false if they are not.
  4. The isValidEmailAddress() and containsURL() functions are NOT exported, which means that they are private and can only be used (invoked/called) from inside the contact-helpers module.
  5. The isValidContactFormSubmit() and sendEmailNotification() function ARE exported, and we'll import them into app.js next.

Now, in app.js, update the route for POST requests to /contact-me to look like this:

app.post('/contact-me', (req, res) => {
  
  // import the contact-helper functions that we need
  const {isValidContactFormSubmit, sendEmailNotification} = require("./modules/contact-helpers");

  // Destructure the req.body object into variables
  const {firstName, lastName, email, comments} = req.body;
  
  // Validate the variables
  if(isValidContactFormSubmit(firstName, lastName, email, comments)){
    // TODO: Everything is valid, so send an email to YOUR email address with the data entered into the form
    res.send("TODO: send an email to my email account")
  }else{
    res.status(400).send("Invalid request - data is not valid")
  }

});

Hopefully the comments explain what's going on.

Now we'll test the contact form by doing the following:

  1. Submit the form without entering any data into it - you should see the 'Invalid request' message
  2. Submit the form by entering data in all inputs, but enter an invalid email address - you should see the 'Invalid request' message
  3. Enter data into all inputs, and make sure the email address IS valid, but add a URL to any of the other inputs - you should see the 'Invalid request' message
  4. Submit the form by entering all valid data into the inputs - you should see the message that says 'TODO: send an email to my email account'.

Finally, we can get started on the code for the sendEmailNotification() function. This is quite complicated and will require you to use email settings from your Western Web Hosting plan.

We'll also use an NPM package named nodemailer that can send emails. When the contact form is submitted, the server will send an email to your Western Hosting Account email address.

Install the nodemailer package by running this command:

npm install nodemailer

Now update the sendEmailNotification() function (in the contact-helpers module) so that it looks like this (this is complicated code, so you don't need to understand all of it):

function sendEmailNotification(message, callback){
  
  // import the node mailer package
  const nodemailer = require('nodemailer');

  const DOMAIN = "YOUR DOMAIN GOES HERE" // ex: mywebsite.com
  const EMAIL_SERVER = "mail." + DOMAIN;
  const EMAIL_ADDRESS = "_mainaccount@" + DOMAIN;
  const EMAIL_PASSWORD = "YOUR cPanel PASSWORD GOES HERE";
  
  // create reusable transporter object using the default SMTP transport
  let transporter = nodemailer.createTransport({
    host: EMAIL_SERVER,
    port: 465,
    secure: true,
    auth: {
      user: EMAIL_ADDRESS, 
      pass: EMAIL_PASSWORD, 
    },
  });

  const email = {
    from: EMAIL_ADDRESS,
    to:EMAIL_ADDRESS,
    subject: 'Contact Submit From Your Website',
    text: message
 };
  
 transporter.sendMail(email, callback);
}

Make sure to set the DOMAIN constant to your website domain (for example, my domain is 'webcoder.club'). And you will also need to use your cPanel password for the EMAIL_PASSWORD constant.

Note that the sendEmailNotification() function is asynchronous, and requires you to pass in a callback function as a parameter. This function will get called when the email is sent successfully, or when an error occurs when sending the email.

Side note

It's not a good idea to put sensitive information, such as your email account password directly into your code. Instead you should store sensitive information in environment variables. We won't be using environment variables in this project, but if you're interested, check out the dotenv package (it's an NPM package that allows you to set and use environment variables).

Now we can update the route that handles the POST requests to /contact-me to look like this:

app.post('/contact-me', (req, res) => {
  
  // import the helper functions that we need
  const {isValidContactFormSubmit, sendEmailNotification} = require("./modules/contact-helpers");

  // Destructure the req.body object into variables
  const {firstName, lastName, email, comments} = req.body;
  
  // Validate the variables
  if(isValidContactFormSubmit(firstName, lastName, email, comments)){
    // Everything is valid, so send an email to YOUR email address with the data entered into the form
    const message = `From: ${firstName} ${lastName}\n
                    Email: ${email}\n
                    Message: ${comments}`;

    sendEmailNotification(message, (err, info) => {
      if(err){
        console.log(err);
        res.status(500).send("There was an error sending the email");
      }else{
        // TODO: render the confirmation page (but for now we'll just send the 'info' param to the browser)
        res.send(info);
      }
    })
    
  }else{
    res.status(400).send("Invalid request - data is not valid")
  }

});

Note the following about this code:

  1. We are constructing a 'message' string from the data that was posted in the form.
  2. We are calling the sendEmailNotification() function and passing in the message, and a callback function.
  3. The code in the body of the callback function checks to see if an error occurred (if the err parameter is truthy).
  4. If there is no error, we send the 'info' param in our response to the browser. This 'info' object will contain information about the email that was sent.

Assuming that you've set the constants correctly, and everything on your Western Hosting account is working, you should be able to submit the contact form (with valid data entered into the form) and see a message that looks something like this (this is the 'info' object):

{"accepted":["_mainaccount@webcoder.club"],"rejected":[],"ehlo":["SIZE 52428800","8BITMIME","PIPELINING","PIPECONNECT","AUTH PLAIN LOGIN","HELP"],"envelopeTime":140,"messageTime":78,"messageSize":404,"response":"250 OK id=1rnpSk-009HYE-2O","envelope":{"from":"_mainaccount@webcoder.club","to":["_mainaccount@webcoder.club"]},"messageId":"<afe04bcd-1731-8784-be25-99ca162189aa@webcoder.club>"}

Also, if you look at your default email account for your Western Hosting, you should see that an email is in your inbox!

Once you've confirmed that you are receiving the emails that are sent by the application, you can finish things off by updating the call to sendEmailNotification() (in app.js) so that it renders a template instead of sending the 'info' object in the response, like so:

sendEmailNotification(message, (err, info) => {
  if(err){
    console.log(err);
    res.status(500).send("There was an error sending the email");
  }else{
    // Render a template that confirms the contact form info was recieved:
    res.render("default-layout", {
      title: "Contact Confirmation",
      content: "<h2>Thank you for contacting me!</h2><p>I'll get back to you ASAP.</p>"
    })
  }
})

Redirecting to HTTPS on the live server

On the live server, we need to ensure that visitors come to our pages using https. Our dev environment does not support https, so we only want to enforce it when the app is running on the live environment (the live server).

We'll add a middleware function that checks the request to see if it's using https, and if not, redirects to the https version of the requested URL.

app.use((req, res, next) => {
   if (process.env.NODE_ENV === 'production') {
      if (req.headers['x-forwarded-proto'] !== 'https')
         // the statement for performing our redirection
         return res.redirect('https://' + req.headers.host + req.url);
      else
         return next();
   }else{
      return next();
   }
});

What else can we do with this project.......

  1. Add client-side input validation to the contact form