Learn JS: FSFS: Section 5 – Starting Complex App


Courses

Updated Jul 5th, 2021

Chapter Details

Ch. 42 – What’s Next?

Ch. 43 – Let’s Begin App #2

Crate new folder on hard drive and open in VSCode. Create app.js file. Run “npm init -y” to get the package.json file. Install express.

Copy and paste reference code.

To stay organized create new “views” folder and home-guest.ejs file and in app.js have app.set(‘views’, ‘views’) and app.set(‘view engine’, ‘ejs’). Install EJS library.

Add a public folder and create a main.css file and copy/past code from the github repo. in app.js have line app.use(express.static(‘public’))

Make sure the <link rel=stylesheet href=”main.css” is actually href=”/main.css”>

Set up auto restarts with nodemon by installing the package and tweaking package.json file to add a new “watch” script that runs “nodemon app.”

Ch. 44 – Always Have the Current Year in the Footer

Did you notice the outdated year down in the footer of our app/template? To have that read 2021 (or whatever the current year is) we can simply edit that bit of the EJS/HTML template and replace the hardcoded 2019 with <%= new Date().getFullYear() %> instead. We’ll learn more about this EJS syntax later in the course, but this quick tip will let your footer date be dynamic.

Ch. 45 – Important Note About Package Versions To Save You Frustration

In the real world you’ll usually want to use the newest version of a package that is available on NPM. However, to make it easier to follow along with the video lessons in this course I strongly encourage you to use the same versions of packages that I’m using. Copy a package.json file from this lesson, delete the node-Modules folder and package-lock.json file and run npm install. Double check the watch script is in package.json or re-implement.

Ch. 46 – What is a Router?

A router lists out urls (or routes). You want to stay organized to have all routes in one file, and that’s all the file does.

First need to know basic skill of sharing code from one file to the next. Use require to bring in packages and also files.

Leverage module.exports to exports and import using const blank = require(‘./blank)

Export whatever you want in node (most commonly an object).

In a new router.js file you need to set:

const express = require('express')
const router = express.Router()
router.get('/', function(req, res) { res.render('home-guest)})
module.exports = router

This will render and ejs template for the homepage. In app.js have a line of code that reads:

app.use('/', router)

Ch. 47 – What is a Controller?

Splitting code into controller file is an industry standard practice. Move function(s) (seconds parameters in routes) to another file and only reference them in router.js. Create a controller folder and userController.js, postController.js, and followController.js files.

In a controller file use alternative to module.exports = {} and instead use exports.login = function(req, res) {} and repeat this anytime you need a function.

In the router we want something like app.get(‘/yourRoute’, userController.home)

create exports.login, exports.home, exports.register, and exports.logout functions

Make sure you import the controller into your router file. In home-guest.ejs update the form action to be ‘/register’ and in the router add a corresponding route and in function.

MVC pattern for organization. Model (logic), View, Controller (accept input and convert to commands).

Controller is the middleman or quarterback and passes relevant data to the appropriate model.

Ch. 48 – Security Note

In the following lessons while you are entering text into the password field in the registration form do not use a real password that you use for any of your real world accounts. Because our local development / testing copy of our application is running on HTTP instead of HTTPS if there was someone on your home network trying to spy on you they could determine your password. So while we are building and testing our application it’s okay to use ridiculously simple and insecure passwords; just don’t use a password you also use for any meaningful accounts.

Ch. 49 – What is a Model?

Hold business logic and enforce rules on our data.

Need to make sure model can get visitor;s input by adding to app.js line of code “app.use(express.urlencoded({extended: false}))” and mind as well “app.use(express.json())” while we are here. Now we can use “req.body” in the userController.

In our model file we want to create an object bit a re-usable blueprint (object). Create a models fo0dler and a User.js file inside and have the following code.

let User = function(data) {
this.homePlanet = "earth",
this.unicorn = data
}
// prototypes will not duplicate to every object
User.prototype.register = function() {}
module.exports = User

Be sure to import User file into the userController.js file and instantiate in a variable named user a new User(req.body)

Ch. 50 – Note about Arrow Functions & the “this” Keyword

Throughout the rest of the course you’ll notice many times where I use a traditional function instead of an arrow function. While I encourage you to use arrow functions where you want to instead of following me character for character, I do want to point out one exception.

In our Model files for this complex app we are using an object-oriented approach to our code. This means there are many cases where our code absolutely does rely on the this keyword pointing towards the object that is calling the method.

Remember, the arrow function isn’t just about a cleaner / more minimal syntax, it also doesn’t change the value of the this keyword to point towards the object that called the method (the way that a traditional function would).

Ultimately, what does this mean? It means when we are writing code together in a Model file, if you use an arrow function where I use a traditional function and then you run into an error, please try to remember that your arrow function is very likely the culprit. You can always test this out by using console.log(this) inside the body of your function to make sure it’s pointing towards what you expect.

Please feel encouraged to use arrow functions anywhere else you desire! They are great, but when you use one try to think about what you want the this keyword to be pointing towards.

Ch. 51 – Adding Validation To Our Model

Create a register prototype function and a validate prototype function.

In the register function you have two broad steps. Step #1: Validate User Data using validate() and Step #2: Only if there are no validation errors then save the user data into the database.

Add an errors array to the User object to push on any errors.

username/email/password cannot be blank, must have a greater than and less than character count.

Install and require in “validator” package instead of using regex for checking email. if !validator.isEmail(this.data.email) push message to error array. Also using validator.isAlphanumeric() to make sure there are no object curly brackets or array brackets.

Ch. 52 – Quick Misc. Clean Up

Don’t let visitors send anything other than a string by creating a function named cleanUp. if (typeof(this.data.username) != “string”) {this.data.username = “”} and duplicate for email and password.

git rid of bogus properties by re-setting/overriding/updating this.data property by manually spelling out properties you want.

Also use .trim() and toLowerCase() for username and password

Ch. 53 – Quick Note about Connecting to Database

If you skipped the “Simple ToDo” app in this course and jumped right to the “Starting Our Complex App” chapter then you will likely run into a problem when attempting to connect to your database in the next lesson. In particular, when you create your MongoDB Atlas account, you need to whitelist your IP address (actually all IP addresses since your address could change and also we don’t know the address of our Heroku server).

Ch. 54 – Connecting To Database

Connect in a reusable fashion. Start a new database for each application. Connect once in a separate file and leverage in each mode file. CReate a db.js file in the root and open connection by installing mongodb package and requiring in package.

const mongodb = require('mongodb)
const conectionString = 'yourStringHereOrProcessENV'
mongodb.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true}, function(err, client) {
module.exports = client.db()
const app = require('./app')
app.listen(3000)
})

We want to a make the db.js file the entry or starting app so we don’t launch application until the database is started. IN app.sj module.exports =app and Cut app.listen(3000) line and add to the above code. Need to update “watch” script in package.json from “nodemon app” to “nodemon db.” In usersCollection require db file and take on .collection(“users”) and now we are ready to run CRUD operations

this.cleanUp()
this.validate()
if (!this.errors.length) {
usersCollection.insertOne({this.data})
}

Ch. 55 – Best Practice Time Out: Environment Variables

Install dotenv package and create new “.env” file and add

CONNECTIONSTRING=yourStringHereWithNoQuotes
PORT=3000

Notice no quotes and all caps variable name. And now in file you need to reference variable require in the ‘dotenv’ package and have line dotenv.config() so it knows to look in .env file and now you are good to reference variable in code with process.env.CONNECTIONSTRING

Also config PORT by adding to config and swapping process.env.PORT for value passed into app.listen() in db.js file. Note that you will manually add these variables in heroku or other hosting environment.

Ch. 56 – Quick Note

In the following lesson when I copy/pasted my connection string into my .env file I accidentally included the closing quote ' at the end of the value. That was not on purpose; do not include the quote.

Ch. 57 – Letting Users Log In

// In ./controllers/userController.js
exports.login = function(req, res) {
let user = new User(req.body)
user.login()
}
// in the ./models/User.js file
User.prototype.login = function() {
this.cleanup()
usersCollection.findOne({username: this.data.username}, function(err, attemptedUser) {if(attemptedUser && attempedUser/password == this.data.password) {CONGRATS} else {INVALID}})
}

Need to tweak the above function to arrow function so the “this” keyword doesn’t change.

The controller should send back res.send() as this is not the job of the model. But it needs to know “when” and how can we wait until login has had a chance to do its job and then do its thing sending a response back to the browser. (Callback approach is the traditional approach, Promises approach). Callback approach is shown at the end of this lesson at the 15-minute mark and it’s a good section to re-watch.

Ch. 58 – What is a Promise? (Part 1)

Replacement to traditional callback approach and is important to understanding modern JS.

The problem with callbacks is callback hell, nested callbacks inside of nested callbacks.

User.prototype.login = function() {
  return new Promise((resolve, reject) => {
  // can now perform async operations
  if (2+2) {resolve()} else {reject()}  
})
}

Better to pass return new Promise() an arrow function to not change the “this” keyword. In userController.js, in the login function you can now leverage the promise just created by chaining onto user.login() .then() and .catch() and passing each of those a function.

// in userController.js file
exports.login = function(req, res) {
let user = new User(req.body)
user.login().then(function(result){
res.send(result)
}).catch(function(err){
res.send(err)
})
}

Rework the login function in the User file to take advantage of mongodb methods which return promises. Note that you can have resolve() multiple times. An example would be one in the .catch() and one in the .then() as part of the else block in an if statement.

Codepen example with the first-pass result of

eatBreakfast()
.then(()=>eatLunch())
.then(()=>eatDinner())
.then(()=>eatDessert())
.catch(function(err){
console.log(err)
})

Ch. 59 – What is a Promise? (Part 2)

But then introduces async/await

async function runOurActions () {
try {
  await eatBreakfast()
  await eatLunch()
  await eatDinner()
  await eatDessert()
}
catch(err) {console.log("Error: " + err)}
}
runOurActions()

Ch. 60 – Running Multiple Promises Efficiently When Order Doesn’t Matter

If you have multiple promises and you want to wait until they’ve all completed before doing something else, but you don’t care what order the promises run/complete in, you can use the following syntax:

async function() {
  await Promise.all([promise1, promise2, promise3, promise4])
  // JavaScript will wait until ALL of the promises have completed
  console.log("All promises completed. Do something interesting now.")
}

There’s no guarantee which one will finish first, but in situations where the ordering of actions isn’t important this will definitely be the fastest way to handle things, as now the promises aren’t blocking each other; they will all begin working at the same time (or within a few milliseconds of each other) and will complete as soon as possible (regardless or order).

Ch. 61 – Hashing User Passwords

Users may use the same password for multiple sites so never store password as raw text in the database. Hashing is a one way street. Example, convert paper into ash but never convert back to paper.

install bcryptjs package and require into User.js

leverage bcryptjs in register function hash user password by creating salt variable and setting to bcrypt.genSaltSynch(10) and then set this.data.password equal to bcrypt.hashSync(a, b) where a is value you want to hash and b is the salt value.

In the login function in User.js model file when comparing username/password values to see if they match you need to use bcryptjs package again via bcrypt.compareSync(a, b) where a is value to check that is not hashed and b is the hashed value from the db.

Note: bcrypt has a maximum value limit so you want to limit password to 50 characters or less.

Ch. 62 – How Can We “Identify” or “Trust” a Request?

Trust subsequent requests after user successfully logs in. Need to keep memory that the user successfully logged in. Two broad methods of sessions (cookies) and tokens (json web tokens or JWT). The course starts with sessions and in API section shows how to use tokens.

Enable sessions by installing “express-session” package and leverage by requiring into app.js and configure a sessionOptions variable

const session = require('express-session')
let sessionOptions = session({
  secret: "my Super Secret Phrase",
  resave: false,
  saveUninitialized: false,
  cookie: {maxAge: 1000 * 60 * 60 * 24,  httpOnly: true}
})
app.use(sessionOptions)

Now your express app supports sessions! In the userController.js file, within the login function body req.session.user = {} where user can be any word you want and you build out the object with any properties and values you want.

Now you can code something like:

exports.home = function(req, res) {
if (req.session.user) {
res.send("Welcome! You are now logged in!)
} else {
res.render("home-guest")
}
}

Browser has session data associated with it so we can trust. The server stores data in memory to the session object AND identifies by automatically adding cookies. See the cookies by going to application tab in chrome dev tools and under storage you see cookies and under your domain you will see connect.sid and that includes a unique value.

Ch. 63 – For Those Who are In a Hurry (Security Note)

For those of you who plan on watching this entire course before creating real world public applications you can stop reading this article and continue to the next lesson. This note is only for people who are in a hurry to launch real world public apps before completing the final chapters of this course.

Before you use cookies and sessions like this in a real world (public) project please be sure to implement CSRF protection. Don’t worry, I don’t expect you to know what “CSRF protection” means; we will learn about it and add it to this application later in the course, step by step together. I just didn’t want to burden us with worries about security this early in the project.

Again, if you plan on completing this course before launching an app publicly you have nothing to worry about. However, if you’re in a hurry, please watch the “What is CSRF?” chapter towards the end of this course before launching an app to the public.

Ch. 64 – Understanding Sessions

Storing values in memory (this is the default for session data) is not persistent at all. We will instead store in the database. install connect-mongo package and in app.js file leverage the package by requiring in

const MongoStore = require('connect-mongo')(session)
// In the sessionsOptions object add
store: new MongoStore({client: require(./db)}),

The bad news is that to this point the db.js file is currently set up to export the database and not the mongodb client. But this is easy to fix in db.js file and tweak module.exports = client.db() to be module.exports = client and now go into the model file and update the usersCollection variable and change from require(‘../db’).collection(“users”) to require(‘../db’).db().collection(“users”)

We can now go into the database backend, mongo atlas, and see a new “sessions” collection and you will see an _id, expires field and a session filed. The session field had a value with our custom data we set attached so we know it’s being stored. The _id value has a value that matches the cookie’s value “connect.sid,” as seen earlier in the browser dev tools application tab. This is what allows us to trust a visitor.

Serve html template for logged in users by creating a home-dashboard.ejs file in the “views” folder. Copy and paste the code from the course repo. In the userController.js file update the res.send(“Welcome to the application”) line to be res.render(‘home-dashboard’)

In order to have the username be shown dynamically in the home-dashboard template send, as the second argument in res.render(), an object with any data you want. In this case {username: req.session.user.username}. In the template swap username for <%= username %>

Ch. 65 – Letting Users Logout

Update form action to “/logout” and jump into router.js to create a new route to handle that request and trigger a userController.logout function that we can create in the controller file.

exports.logout = function(req, res) {
req.session.destroy()
res.send("You are logged out")
} 

This will destroy the document in the database but we want to use res.redirect() instead of res.send() but will need to wait for the destroy() method to complete before redirecting.

exports.logout = function(req, res) {
req.session.destroy(function() {
res.redirect("/")
})
}
//using callback approach because destroy() does not return a promise

Do the same for the login function to redirect to the homepage url but care for the timing of session data since saving data in the database takes time. And we won’t know how long this takes so we manually tell the session data to save by having req.session.save() and pass a callback function that has the res.redirect(‘/’) code.

Ch. 66 – Adding Flash Messages

If you fail a login attempt you want to redirect back to home-guest page with a flash message by installing “connect-flash” and leverage in app.js by requiring in. Just under app.use(sessionOptions) add app.use(flash()) middleware.

Now in userController login function, in the home function, you can pass a second argument but we need to remember stateless nature of http requests. So in the login function in the function in the catch block above res.redirect(‘/’) add req.flash(‘errors’, ‘e’) where e could be a string of a message or, in this case, e for the error found in the catch function.

req.flash() helps us add or remove data from our session object in the database but does take some time to complete so we need to wait for this to finish before running the next line of code so right after this we manually tell the session data to save and put the code we wan to run in a callback function

.catch(function(err){
req.flash('errors', err)
req.session.save(function(){
  res.redirect('/')
})
})

Now in the exports.home function in the userController pass res.render a second argument of {errors: req.flash(‘errors’)}

Note that we don’t only want to show the message but we want to delete it right after, since we only want to show it once. Which is why we use the flash package, it handles this for us.

In the home-guest template run a forEach to show the flash message.

Ch. 67 – User Registration Improvements (Part 1)

Task #1: Redirect with validation messages and show flash messages using .forEach() to add errors to new regErrors array. Pass this data to the template and show in template.

Task #2: Add validation rules to make sure username and email are unique. Only check the databsse if it passes length and validator check. Push error on errors array if there’s an error. Check database using the .findOne method which returns a promise. Coordinate the timing by awaiting .findOne and converting validate function to async function by adding async keyword.

Ch. 68 – User Registration Improvements (Part 2)

To finish task#2 from the previous lesson, in the User.js model file we need to await .validate() in the User.prototype.register function. To accomplish this we will convert the validate function to return a promise. Now we can use the await keyword before the this.validate() function call.

Note that a promise does not need to reject. In this case there is only .resolve() as the very last line of the function to signal that it is completed. Resolve() has no code inside of it.

We will also need to convert the user.register() function to return a promise as well. Then we can await this.register() in the controller’s register function and convert that to be an async function. Actually he used the .then().catch() approach (instead of async/await) by giving them both arrow functions with some logic.

Ch. 69 – Adding User Profile Photos

This feature uses the gravatar.com service, which associates a user’s image with an email address, and may not be necessary for your app.

User.protype.getAvatar = function() {
this.avatar = `https://gravatar.com/avatar/${md5(this.data.email)}?s=128`
}

install the md5 package to use the hashing algorithm the gravatar service expects and leverage by requiring in the User.js file.

Call the getAvatar function at the appropriate time (in the login function and in the register function). Call the getAvatar function after the database actions so the url is generated on the fly, and not stored in the database permanently, in case the gravatar service ever changes the url structure in the future.

Set an avatar field on the session in the login and register functions in the userController file.

Pass the data into your templates in the exports.home function in the userController.js file.

Leverage the data in the home-dashboard ejs template dynamically in the image source attribute.

When a user logs in they only provide and username and password so in the login function in the User.js model file you will need to look up the email from the database so above the this.getAvatar() function call have this.data = attemptedUser and now when the getAvatar function runs it will have access to the email address.