Model - View - Control Implementation

Conceptual Overview

The video provides a high level overview of Model View Control. This is the Transcript of the video.

Activity Overview

The video provides a general overview of the activity, but does not contain the detail needed to complete each process. Watch the video to obtain a general idea, but follow the written steps to complete the activity. This is the Transcript of the video.

Get Organized

We'll begin by creating several new files and storing these into existing folders.

  1. Create a new index.js file in the database folder.
  2. Create a new inventory-model.js file in the models folder.
  3. Create a new baseController.js file in the controllers folder.

Database Connection

With the database created, we need a means for our application to talk to it. That is what we'll do now.

  1. Open the package.json file.
    • Look at the list of dependencies.
    • You'll find "pg" in the list. "pg" is a short-hand reference to the node-postgres package that Express can use to talk with a PostgreSQL database.
    • Open the node-postgres Documentation in your browser.
    • Bookmark this documentation, so you can refer back to it as needed.
    • You can close the package.json file. I just wanted you to know that this package was listed and should be installed already.
  2. Find and open the .env file, which should be at the root of the project. Make sure that the name is ".env" and not ".envsample".
  3. Add the following code to the .env file:
    NODE_ENV='development'
    This code helps us to identify when we are working and testing locally vs in a remote, production environment, which will be done later. Save and close the file.
  4. Open the index.js file in the database folder.
  5. Add the code shown to the index.js file:
    const { Pool } = require("pg")
    require("dotenv").config()
    /* ***************
     * Connection Pool
     * SSL Object needed for local testing of app
     * But will cause problems in production environment
     * If - else will make determination which to use
     * *************** */
    let pool
    if (process.env.NODE_ENV == "development") {
      pool = new Pool({
        connectionString: process.env.DATABASE_URL,
        ssl: {
          rejectUnauthorized: false,
        },
    })
    
    // Added for troubleshooting queries
    // during development
    module.exports = {
      async query(text, params) {
        try {
          const res = await pool.query(text, params)
          console.log("executed query", { text })
          return res
        } catch (error) {
          console.error("error in query", { text })
          throw error
        }
      },
    }
    } else {
      pool = new Pool({
        connectionString: process.env.DATABASE_URL,
      })
      module.exports = pool
    }
    
  6. An Explanation

    • Line 1 - imports the "Pool" functionality from the "pg" package. A pool is a collection of connection objects (10 is the default number) that allow multiple site visitors to be interacting with the database at any given time. This keeps you from having to create a separate connection for each interaction.
    • Line 2 - imports the "dotenv" package which allows the sensitive information about the database location and connection credentials to be stored in a separate location and still be accessed.
    • Line 3-8 - a multi-line comment concerning the "ssl" code found in the connection pool function. This is critical code. Be sure to read and understand the comment..
    • Line 9 - creates a local pool variable to hold the functionality of the "Pool" connection.
    • Line 10 - an if test to see if the code exists in a developent environment, as declared in the .env file. In the production environment, no value will be found.
    • Line 11 - creates a new pool instance from the imported Pool class.
    • Line 12 - indicates how the pool will connect to the database (use a connection string) and the value of the string is stored in a name - value pair, which is in the .env file locally, and in an "environment variable" on a remote server. These are equivelent concepts, but different implementations.
    • Lines 13 through 15 - describes how the Secure Socket Layer (ssl) is used in the connection to the database, but only in a remote connection, as exists in our development environment.

      A Short Explanation

      SSL is a means of excrypting the flow of information from one network location to another. In this case, when we attempt to communicate with the remote database server from our own computer the code indicates that the server should not reject our connection. However, when we work in a remote production server, the ssl lines must not exist. This is because our application server and the database server will be in the same system and their communication will be secure, which is what we will require.

    • Line 16 - ends the pool function started on line 11.
    • Line 17 - left blank.
    • Lines 18-19 - comments related to the function to be exported in lines 21 through 31.
    • Lines 20-31 - exports an asynchronous query function that accepts the text of the query and any parameters. When the query is run it will add the SQL to the console.log. If the query fails, it will console log the SQL text to the console as an error. This code is primarily for troubleshooting as you develop. As you test the application in your development mode, have the terminal open, and you will see the queries logged into the terminal as each is executed.
    • Line 32 - ends the if and opens the else structure.
    • Line 33 - creates a new "pool" instance from the "Pool" class.
    • Line 34 - indicates the value of the connection string will be found in an environment variable. In the production environment, such a variable will not be stored in our .env file, but in the server's settings.
    • Line 35 - ends the Pool object and instance creation.
    • Line 36 - exports the pool object to be used whenever a database connection is needed. This is for the production environment, which means the queries will not be entered into the console.
    • Line 37 - ends the else structure.
  7. Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
  8. Save and close the file.

Add the Connection String to the .env file

In the database file that you just built, the code indicates that the connection pool will find the information it needs to talk with the database server in the .env file. Let's get that inserted, so it is there.

  1. In your browser, go to render.com and login using your GitHub credentials.
  2. Open the database server in the dashboard.
  3. Scroll down to the "Connections" area.
  4. Click the "Copy" button on the "External Database URL" line.
  5. Open the .env file in VSC.
  6. On a blank line type, DATABASE_URL=, then paste the string copied from render.com. Make sure there is no space in the entire string.
  7. Confirm that no warnings or errors appear in the .env file.
  8. Save and close the file.

When you get ready to run and test your application, the database connection pool will look in the .env file for a variable of DATABASE_URL. When found, it will use the connection string to attempt to talk to the database server. If successful, the pool will then have the ability to run the query in the inventory-model file, which you will build next, and return the data from the server.

The Inventory Model

With the database connection pool created we can now use it to begin interacting with the database. I hope you'll remember that in an M-V-C approach, the "Model" is where all data interactions are stored. Our inventory-model.js document is where we'll write all the functions to interact with the classification and inventory tables of the database, since they are integral to our inventory.

  1. Open the inventory-model.js file in the models folder.
  2. Type the code shown in the image into the file:
    const pool = require("../database/")
    
    /* ***************************
     *  Get all classification data
     * ************************** */
    async function getClassifications(){
      return await pool.query("SELECT * FROM public.classification ORDER BY classification_name")
    }
    
    module.exports = {getClassifications}
    
  3. An Explanation

    • Line 1 - imports the database connection file (named index.js) from the database folder which is one level above the current file. Because the file is index.js, it is the default file, and will be located inside the database folder without being specified. The path could also be ../database/index.js. It would return the same result.
    • Line 2 - left intentionally blank.
    • Lines 3-5 - a multi-line comment introducing the function.
    • Line 6 - creates an "asynchronous" function, named getClassifications. An asynchronous function returns a promise, without blocking (stopping) the execution of the code. It allows the application to continue and will then deal with the results from the promise when delivered.
    • Line 7 - will return (send back) the result of the SQL query, which will be sent to the database server using a pool connection, when the resultset (data) or an error, is sent back by the database server. Notice the two keywords: return and await. Await is part of the Async - Await promise structure introduced in ES6. Return is an Express keyword, indicating that the data should be sent to the code location that called the function originally.
    • Line 8 - ends the function began on line 6.
    • Line 9 - left intentionally blank.
    • Line 10 - exports the function for use elsewhere.
  4. Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
  5. Save and close the file.

The base controller

A controller is the location where the logic of the application resides. It is responsible for determining what action is to be carried out in order to fulfill requests submitted from remote clients. The "base" controller will be responsible only for requests to the application in general, and not to specific areas (such as inventory or accounts).

  1. Find and open the baseController.js file that should be in the controllers folder.
  2. Add the code shown below to the file:
    const utilities = require("../utilities/")
    const baseController = {}
    
    baseController.buildHome = async function(req, res){
      const nav = await utilities.getNav()
      res.render("index", {title: "Home", nav})
    }
    
    module.exports = baseController
    
  3. An Explanation

    • Line 1 - imports an index.js file (which does not yet exist) from a utilities folder (which does not yet exist) which is one level above the current location inside the controllers folder.
    • Line 2 - creates an empty object named baseController.
    • Line 3 - left intentionally blank.
    • Line 4 - creates an anonymous, asynchronous function and assigns the function to buildHome which acts as a method of the baseController object. In short, this is similar in concept to creating a method within a class, where baseController would be the class name and buildHome would be the method. Being asynchronous, it does not block (stop) the application from executing while it awaits the results of the function to be returned. The function itself accepts the request and response objects as parameters.
    • Line 5 - calls a getNav() function that will be found in the utilities > index.js file. The results, when returned (notice the "await" keyword), will be stored into the nav variable.
    • Line 6 - is the Express command to use EJS to send the index view back to the client, using the response object. The index view will need the "title" name - value pair, and the nav variable. The nav variable will contain the string of HTML code to render this dynamically generated navigation bar.
    • Line 7 - ends the function started on line 4.
    • Line 8 - left intentionally blank.
    • Line 9 - exports the baseController object for use elsewhere.
  4. Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
  5. Save and close the file.

Differences in Implementation

In the inventory-model.js file, you should have noticed that the function was created, and then exported within an unnamed object, e.g.:

module.exports = { getClassifications }

However, in the baseController.js file, a named object was created on line 2:

const baseController = {}

Then, the function was built as a named method of the object on line 4: baseController.buildHome = async function(req, res) { ...

At the bottom of the controller the named object is exported: module.exports = baseController

Both of these methods do the exact same thing. In fact, there are additional ways of doing the same thing. But the code found in the examples throughout the course are limited to these two. You may ask, "Why not use one way for everything?" The answer is that different organizations approach things differently, and we want you to see and use a variety. Just know, this approach is intentional.

Alter the "Index Route"

With the model and controller in place, it is time to alter the route to deliver the index.ejs view using the entire M-V-C approach.

  1. Find and open the server.js file, at the root of the project.
  2. Locate the "Require Statements" area, near the top of the file. Add a new require statement to bring the base controller into scope:
const baseController = require("./controllers/baseController")
  1. Save the file.
  2. Locate the "Index" route in the "Routes" area of the file.
  3. Alter the route to match this image:
    Screenshot of code to call the base controller method in the server.js file.
  4. An Explanation

    • Previously the index.ejs view was rendered directly in the route. While it worked, it did not follow the M-V-C methodology.
    • The new code uses the imported "baseController" to "call" the "buildHome" method.
    • This will execute the function in the controller, build the navigation bar and pass it and the title name-value pair to the index.ejs view, which will then be sent to the client.
  5. While we are close to testing, there is one more loose thread to tie up.
  6. Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
  7. Save and close the file.

Build the Utilities file and functions

In the baseController file we wrote code to import a file from the utilities folder. That file will contain a function, which will use the inventory model to get the data from the database, then will build an HTML unordered list around the data. The finished HTML will be sent back as a string to be used in the navigation partial to render the actual navigation bar, based on the vehicle classification found in the database. Let's build the function, then test.

The "getNav" Function

  1. In VSC click at the very bottom of the explorer pane so that no folder or file is selected.
  2. Click the "New File..." icon.
  3. Type "utilities/index.js", press the "Enter" key.
  4. A new folder named utilities should appear with an index.js file within it.
  5. The new index.js file should be open in the work area. If not, open it.
  6. This file will hold functions that are "utility" in nature, meaning that we will reuse them over and over, but they don't directly belong to the M-V-C structure.
  7. In the file, type the code shown below:
    const invModel = require("../models/inventory-model")
    const Util = {}
    
    /* ************************
     * Constructs the nav HTML unordered list
     ************************** */
    Util.getNav = async function (req, res, next) {
      let data = await invModel.getClassifications()
      let list = "<ul>"
      list += '<li><a href="/" title="Home page">Home</a></li>'
      data.rows.forEach((row) => {
        list += "<li>"
        list +=
          '<a href="/inv/type/' +
          row.classification_id +
          '" title="See our inventory of ' +
          row.classification_name +
          ' vehicles">' +
          row.classification_name +
          "</a>"
        list += "</li>"
      })
      list += "</ul>"
      return list
    }
    
    module.exports = Util
    
  8. An Explanation

    • Line 1 - requires the inventory-model file, so it can be used to get data from the database.
    • Line 2 - creates an empty Util object. Just as you did earlier in the base controller.
    • Line 3 - left intentionally blank.
    • Lines 4 through 6 - a multi-line comment introducing the function.
    • Line 7 - creates an asynchronous function, which accepts the request, response and next methods as parameters. The function is then stored in a getNav variable of the Util object.
    • Line 8 - calls the getClassifications() function from the inventory-model file and stores the returned resultset into the data variable.
    • Line 9 - creates a JavaScript variable named list and assigns a string to it. In this case, the string is the opening of an HTML unordered list. Note the use of let. This is because the value will be changed as the upcoming lines of the function are processed.
    • Line 10 - the list variable has an addition string added to what already exists. Note the use of +=, which is the JavaScript append operator. In this instance a new list item, containing a link to the index route, is added to the unordered list.
    • Line 11 - uses a forEach loop to move through the rows of the data array one at a time. For each row, the row is assigned to a row variable and is used in the function.
    • Line 12 - appends an opening list item to the string in the list variable.
    • Line 13 - appends the code that is found on lines 14 through 20 as a string to the list variable.
    • Line 16 - a string that includes the beginning of an HTML anchor. The + sign is the JavaScript concatenation operator, used to join two strings together. The value in the href attribute is part of a route that will be watched for in an Express router.
    • Line 17 - the classification_id value found in the row from the array. It is being added into the link route.
    • Line 18 - the title attribute of the anchor element.
    • Line 19 - the classification_name value found in the row from the array. It is being added into the title attribute.
    • Line 20 - the last part of the string forming the opening HTML anchor tag.
    • Line 21 - the classification_name from the row being displayed between the opening and closing HTML anchor tags. This is the display name in the navigation bar.
    • Line 22 - the closing HTML anchor tag.
    • Line 23 - the closing list item tag being added to the list variable.
    • Line 24 - the ending of the forEach loop and enclosed anonymous function.
    • Line 25 - the closing HTML unordered list tag being appended to the list variable.
    • Line 26 - sends the finished string, back to where the getNav function was called.
    • Line 27 - ends the function which started on line 7.
  9. Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
  10. Save the file.

If you look at the code (lines 7 to 24) carefully, it should be somewhat familiar. It simply keeps adding strings to a variable. When done, the entire string is nothing more than an HTML unordered list that contains some anchor elements. When the function is done building the string, the variable is sent to where the function was called.

Where does this leave us?

The flow of the M-V-C for our application runs something like this, so far:

  1. A client types or clicks a link containing the index route of our application.
  2. That triggers a request to our server.js page.
  3. The route is found in the page, which calls the function in the baseController file.
  4. The controller calls the getNav function and awaits the result to be returned.
  5. When the finished string is returned, it is stored in the nav variable.
  6. The controller, then tells Express and EJS to send the index.ejs file back to the client, and send the nav string and title name-value pair along with it. These items will be used in the head.ejs and navigation.ejs partials.
  7. The server builds the finished home page and sends it to the browser.

Adjust the navigation.ejs Partial File

With all the code in place, there is one more minor change to be made, then we can test.

  1. Find and open the navigation.ejs file in the partials folder.
  2. Highlight and delete the existing unordered list that you typed as static code previously.
  3. Type this code where the previous code was located: <%- nav %>
  4. When done, the navigation.ejs partial should look like this:
    Screenshot of code for the finished navigation.ejs partial file.
  5. Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
  6. Save and close the file.

Time to Test

  1. Save all of your work.
  2. Open a new VSC terminal window.
  3. Type pnpm run dev, press "Enter".
  4. The node server should start showing that it is running on localhost:5500
  5. In a browser, type http://localhost:5500/.
  6. If things work as they should, you should be looking at your home page.
  7. Hover your mouse over any of the navigation items. You should now see a tooltip generated from the title attribute. You'll recall that the original, static, navigation items did not have this. But, your dynamic navigation items do!
  8. You should also be able to look in the terminal pane and see the query that was run when the page loaded. It is the same query that exists in the inventory-model, and is being written in the terminal as part of the code we placed in the database pool code, found in the database > index.js file.
  9. If things didn't work, check spelling, file and folder names and the path. Talk to your learning team, the TA or the professor. Be sure that you can get it to work.
  10. When done testing, be sure to stop the server with Control + C in the terminal.