The Complete Node / The Complete Guide -> Table of Contents:
Any word that is highlighted in yellow represents a "tooltip." You can hover over it and see some additional information about that particular term. Any edits to that functionality can be investigagted by heading out to tooltipster.com.
UDEMY Course by Maximilian Schwarzmüller
To move a Node application from one box to another, you don't want to move the "node_modules" directory in that it's a huge folder and Node has something in place tha tmakes for a much easier and accurate way of moving things around. Simply move your code sans the node_modules directory and then run npm install in your Terminal after navigating to that directory
Visual Studio Shortcuts -> https://vslive.com/Blogs/News-and-Tips/2015/04/5-VS-Keyboard-Shortcuts.aspx "Captain's Log" - elaborations on certain core concepts that merited some additional notes (you'll see these referenced as graphic alerts as well)... A) What is Node? 1) Runtime B) REPL A) Characteristics 1) Weakly Typed Language 2) Datatypes Can Be Switched 3) Object Oriented Language 4) Primitive and Reference Data Types 5) Versatile Language 6) Wide Variety of Tasks B) Core Syntax Review C) let, var and const 1) ECMA 2) let 3) const D) Arrow Functions 1) Anonymous vs Named Functions 2) "this" 3) Some Shorthand E) Working with Objects, Properties and Methods 1) Objects a) Arrays map push F) Understanding Spread and Rest Operators 1) Spread a) slice b) spread 2) Rest G) Destructuring H) Async Code and Promises 1) Asynchronous vs Synchonous 2) callback 3) constructor 4) Promises I) Template Literals A) How the Web Works B) Set up a Server 1) Core Modules 2) Setup Code C) Event Loop D) Sending Responses E) Request and Response Headers F) Routing G) Redirecting Requests H) Parsing Requests I) Understanding Event Driven Code Execution 1) Event Loop a) Call Stack b) Blocking c) Concurrency & Web API's 2) The "better code..." J) Blocking and Non-Blocking Code / writeFileSync vs writeFile K) Behind the Scenes 1) Worker Pool 2) Serious Event Loop L) Using the Node Modules System M) Homework A) NPM Scripts 1) npm init B) Third Party Packages 1) nodemon a) --save-dev b) -g 2) Core Concepts 3) Using Nodemon C) Finding and Fixing Errors A) Quick Review - Setup a New File 1) npm init 2) nodemon 3) shortcuts B) Express Setup C) Middleware D) How Middleware Works E) Behind the Scenes F) Different Routes G) Parsing Incoming Requests H) Limiting Middleware Execution to POST Requests I) Using Express Router J) Adding a 404 Error Page K) Filtering Paths L) Writing HTML M) Rendering HTML N) Returning a 404 Page O) Navigation Helper Function P) Serving Files Statically (CSS) A) Sharing Data Across Requests and Users B) Template Engines 1) Pug Code and Dynamic Content 2) Using Layouts in Pug i) app.js ii) main-layout.pug iii) admin.js iv) shop.js v) add-product.pug 3) Handlebars 4) Convert Project to Handlebars i) add-products.hbs ii) shop.hbs 5) Using Layouts in Handlebars i) app.js ii) main-layout.hbs iii) admin.js iv) shop.js v) add-product.hbs 6) EJS i) 404.ejs - head.ejs - navigation.ejs - end.js ii) add-product.ejs iii) shop.ejs I've got things organized a little differently in this section. Rather than a conventional "outline" format, I listed everything in order, as far as how it's constructed and then explained. Here we go... A) Setup This first section details what you need to do to set up a basic Node app and then goes through the actual "app.js" file and demonstrates the little bit of code that you need to have in place to constitute a basic beginning for an advanced application. 1) Set Up Directory 2) npm init 3) Install Nodemon 4) Setup Shortcuts 5) Install Express i) Starting Point of a Working App B) app.js 6) require("path") 7) const express=require('express'); 8) const bodyParser = require('body-parser'); 9) const app = express(); 10) view engine 11) create routes 12) app.use(bodyParser.urlencoded()); 13) express.static(path.join(__dirname, 'public')) 14) app.use("/admin", adminData.routes); 15) 404 page C) routes/admin.js 1) require('path'); 2) const express 3) const rootDir 4) const router 5) const products=[] 6) router.get 7) router.post 8) exports.routes 9) exports.products D) routes/display.js 1) require('path'); 2) const express 3) const rootDir 4) const adminData 5) const router 6) router.get 7) exports.routes E) utility/path.js F) header.ejs G) footer.ejs H) navigation.ejs 1) Navigation "IF Statement for "Display" Page 2) Navigation "IF Statement for "Admin" Page I) public / css J) add-name.ejs K) admin.js 1) const path = require("path"); 2) const express = require("express"); 3) const rootDir = require("../utility/path"); 4) const rourter = express.Router(); 5) products=[]; 6) router.get... 7) router.post... 8) exports.router=router; 9) exports.products=products; A) The Controller 1) admin.js 2) shop.js 3) product.js B) The Model 1) product.js (controller) 2) product.js (model) A) How You're Going to Change the Model 1) File System (fs) 2) path a) join b) dirname c) process.mainModule.filename 3) readFile a) JSON.parse 4) products.push(this) 5) fs.writeFile A) static fetchAll(); 1) path.join 2) process.mainModule.filename 3) readFile 4) return JSON.parse(fileContent); B) Callback - The Sequel 1) inner function A) Helper Function A) Add a Route B) Add a Method to the Controller C) Basic HTML vs Content (Callback review) 1) Basic HTML 2) Dynamic Content a) Controller b) View A) Add Button B) Add ID C) Retrieve ID 1) The Order of Your Routes A) The Controller B) The Model C) The View A) Add to Cart 1) The View 2) The Route 3) The Controller B) Add to Cart as an Include A) Laying Down Some Basics 1) const fs 2) const p B) Adding a Product to Your Cart (exp (exploded view) 1) err -> callback 2) fetching, analyzing, adding -> exploded view i) find function ii) spread operator iii) concatenate array (ES6 using spread operator) A) Router B) Controller 1) "Edit" Query Parameter C) View A) Controller B) View A) product.ejs A) Router B) Controller C) Model 1) construct A) Router B) Controller C) Model 1) product.js 2) cart.js A) Router B) Controller A) Router B) Controller C) Model Fix (slight bug repair to the "deleteProduct" function) A) SQL B) NoSQL C) Differences and Advantages 1) ACID 2) Scalability A) Installing MySql Package 1) MySql Workbench Notes B) Establishing Database Connection C) Creating a Table D) SELECT Statement 1) Model 2) Controller E) INSERT Statement 1) Model 2) Controller E) View Details 1) Model 2) Controller A) Definition B) Connecting to Database C) Defining a Model D) Creating and Inserting a Product D) Retrieving Products E) Retrieving One Product 1) findByPk 2) where: { id: prodId } E) Editing a Product F) Deleting a Product G) Adding a One to Many Relationship H) Adding a Dummy User 1) Establishing User as Part of the Request Object I) Using Magic Methods J) One to Many & Many to Many Relationships K) Creating and Fetching a Cart 1) Create Cart 2) Retrieve Cart L) Adding Products to the Cart 1) Adding a Brand New Product 2) Adding an Existing Product M) Deleting Products From the Cart N) Adding an Order 1) Setting up Model 2) Setting up Router and Controller O) Resetting the Cart and Outputting Orders 1) Reset the Cart 2) Router Error 3) Outputting Orders A) Basics 1) BSON 2) Relationships 3) Installation 4) Connection i) Connection Pool B) Using Database Connection 1) products.js Model 2) admin.js Controller C) Mongo DB Compass D) Retrieving Products 1) Model 2) Controller E) Fetching a Single Product 1) Cursor 2) ObjectId 3) Model 4) Controller F) Editing a Product 1) Controller (Product Display) 2) Controller (Product Edit) 3) Model (Product Edit) G) Deleting a Product 1) Model 2) Controller H) Ternary "IF" Change on save() I) Adding a User 1) Model (user.js) 2) Controller (app.js) J) Adding a Product w/ User 1) Controller (admin.js) 2) Model (product.js) K) Cart Items & Orders 1) User Model (user.js) 2) app.js (add more substance to "req.user") L) Storing Multiple Items in the Cart M) Displaying Cart N) Deleting Cart Items O) Adding an Order 1) users.js (model) 2) shop.js (controller) A) ORM vs ODM B) Installing Mongoose and Connecting to the Database 1) Install Mongoose 2) Using Mongoose to Connect to the Database C) Creating the Schema D) Save a Product E) Fetch All Products F) Fetch A Single Product G) Edit A Product H) Delete A Product I) Adding and Using a User Model 1) user.js Model 2) app.js J) Using Relations in Mongoose 1) product.js 2) admin.js K) Additional Notes for Managing Relations in Mongoose 1) Retrieving the Entire User Object 2) Specifying Which Data You Want to Retrieve L) Adding User Data to the Shopping Cart 1) user.js Controller 2) user.js Model M) Loading the Cart w/ Product Information (populate) N) Delete Cart Item O) Add Order 1) user collection 2) shop.js Controller P) Display Orders A) Cookie Defined B) Creating the Login Form 1) Create Your Route 2) Register Your Route in app.jst 3) Create Your Controller 4) Create Your View C) Creating a Cookie D) Manipulating a Cookie (Max-Age, Secure, HttpOnly) E) Sessions F) Installing Session Middleware G) Using Session Middleware H) Using MongoDB to Store Sessions 1) app.js 2) auth.js I) Deleting a Cookie (Logout) 1) nav.js (your button) 2) auth.js 2) auth.js (your Controller) J) Fixing Some Bugs 1) navigation.ejs 2) Display Cart This is the real world project I was tasked with building that transformed a working PHP application into a MERN app. A) Basic Setup 1) app.js 2) start.js (route) 3) start.js / auth.js (controller) 4) index.ejs / login.ejs (view) B) Security 1) Necessary Middleware and Packages a) Mongoose i) app.js b) Express Session i) app.js c) MongoDB Session i) app.js d) bcryptjs i) auth.js A) Signup Form and Insert Code 1) signup.ejs 2) auth.js (Controller) B) Encryption / bcryptjs C) Login Controller (auth.js) D) Route Protection 1) Using a Line by Line Approach 2) Using Middleware E) Understanding and Preventing CSRF Attacks F) Providing User Feedback 1) Import / Register it in "app.js" 2) Add it to postLogin on "auth.js" Controller 3) Add it to the "login.ejs" View A) sendgrid.com B) auth.js A) Resetting Passwords 1) reset-password.ejs 2) getReset Controller 3) postReset Controller 4) getNewPassword Controller a) reset password link / route b) the Controller c) the reset-password view 5) postNewPassword Controller B) Adding Authorization 1) Displaying Products 2) Editing Products 3) Deleting Products A) Basic Email Validation 1) auth.js Routes 2) auth.js Controller B) Using the Error Message Field C) Custom Validator Fields D) More Validators E) Equality F) Async Validation G) Keeping User Input 1) signup.ejs 2) auth.js Controller H) Conditional CSS Classes 1) auth.js Controller 2) signup.ejs Controller 3) forms.css I) Sanitizing Data J) Validating Product Info 1) Validator Package 2) admin.js Controller A) Error Theory 1) throw 2) try / catch (syncronous) B) Throwing Errors in Code (app.js) C) Returning Error Pages 1) Regular Page D) Using Express Middleware for Errors E) Using Express Middleware for Errors-> Correctly F) Errors & Http Response Codes A) What Are They? B) Data Formats and Routing 1) JSON 2) Routing C) Core Principles D) Sending Requests and Responses | CORS Errors 1) Codepenn| POST REQUEST / GET REQUEST This is a practical application involving a React UI with a real live API dynamic! A) Retrieving Posts 1) Feed.js (React) 2) feed.js (Node Controller) B) Creating Posts C) Adding Validation 1) React Validation 2) Adding Server Side Validation D) Adding a Database E) Static Images and Error Handling 1) Static Images 2) Elegant Errors F) Displaying a Single Post G) Uploading an Image H) Updating Posts I) Deleting Posts 1) feed.js (route) 2) feed.js (controller) J) Pagination 1) feed.js (front end) 2) feed.js (Controller) K) Building a User Model (route, model, app.js) L) Adding User Validation (auth.js [route], auth.js [controller]) M) Signing Users Up 1) bcryptjs 2) auth.js (Controller) 3) App.js (front end) N) How Validation Works (JWT) O) Validating User Login 1) auth.js (Controller) P) Logging in and Creating JSON Web Tokens (JWTs) 1) auth.js (route) 2) auth.js (controller) 3) App.js (front end) Q) Using and Validating the Token 1) Feed.js 2) is-auth.js 3) feed.js (route) R) Adding Auth Middleware to All Routes and Methods 1) Feed.js (front end) 2) SinglePost.js S) Connecting Posts to Users 1) post.js (model) 2) feed.js (controller) T) Adding Authorization Checks U) Clearing Post-User Relations V) Getting Rid of "Unexpected token < in JSON at position 0" A) Intro B) socket.io C) Making it Work 1) Installation i) app.js (api) ii) Feed.js (react) D) Identifying Realtime Potential E) Sharing the IO Instance Across Files F) Synchronizing POST Additions 1) Adding IO to feed.js (Controller) 2) Feed.js (front end) G) Adding Name to Posts H) Updating Posts on All Connected Clients 1) Controller 2) Feed.js (front end) I) Sorting Correctly J) Deleting Posts 1) feed.js Controller 2) Feed.js (front end) A) What is GraphQL B) Setup and Our First Query 1) Setup 2) Our First Query i) app.js ii) schema.js and resolvers.js C) Defining a Mutation 1) schema.js D) Adding a Mutation Resolver & GraphiQL 1) schema.js 2) resolvers.js 3) GraphiQL E) Adding Validation 1) validator | resolvers.js F) Handling Errors G) Hooking Up the Front End 1) CORS Error A) EADDRINUSE A) AWS
A) What Is Node? (back to top...) Node give you the chance to run JavaScript beyond the browser. You can run it on the server or in any enviornment for that matter. So, whereas JavaScript is something limited to your web browser, Node has no limitations in that regard. To make this happen, it uses an engine crafted by Google called "V8." It compiles JavaScript and runs it as "machine code." In addition, it adds some additional features beyond your typical JavaScript dynamic. For example, you can access files directly on you machine. JavaScript can't do that. 1) Runtime (back to top...) One term that we ran into here was "runtime," which refers to the period of time a program is up and running. JavaScript is based on Java. Java is compiled code. By that, we mean that there's a layer of "processing" that happens between the code that is written and the "machine code" that is actually executed. JavaScript, on the other hand, is happening right at runtime in that it's "interpreted" code. In some ways, this is happening regardless of the language that you're using in that everything is being converted to binary code in some way, shape or form. The basic difference is that a compiler system, including a (built in or separate) linker, generates a stand-alone machine code program, while an interpreter system instead performs the actions described by the high level program. That being the case, compiled code will be compiled before runtime, interpreted code happens right at runtime. B) REPL (back to top...) If you write "node" in your Command Line, at that point you've entered into the "REPL" dynamic. REPL stands for "Read, Eval, Print, Loop." In some ways, you can think of it as a stand-alone "sandbox" where you can proof the accuracy of your code. More on that later... A) Characteristics (back to top...) 1) Weakly Typed Language (back to top...) By "weakly," we mean that while JavaScript does recognize strings, booleans and integers, it doesn't insist that you define the datatype in the context of creating a variable. For example, with PDO, you have to define the parameter as an "INT" or a "STR." JavaScript doesn't make you do that. 2) Datatypes Can be Switched (back to top...) You can have a variable that starts out in your code as a "string," and then switch it to be an "integer" and JavaScript does not freak out. 3) Object Oriented Language (back to top...) Remember, an "object" is a combination of properties and values. JavaScript can be organized in that way. 4) Primitive and Reference Data Types (back to top...) Also, JavaScript makes use of the "Primitive" and "Reference" types. Click here for an indepth explanation, but the bottom line is that you have two types of data. You've got the "primitive" type which is going to be either undefined, null, boolean, number, string, or symbol. The other type of data is "Reference" and that's going to be an "object." All that to say, that when you're manipulating data, if the variable stores a primitive value, when you manipulate its value, you are working on the actual value stored in the variable. In other words, the variable that stores a primitive value is accessed by value. Unlike the primitive value, when you manipulate an object, you are working on the reference to that object, rather than the actual object. In short, a variable that stores an object is accessed by reference. There...! 5) Versatile Language (back to top...) You can run JavaScript on a server or on your own box - which is what Node.js is all about. 6) Wide Variety of Tasks (back to top...) JavaScript can also provide a wide variety of tasks! We'll see more of that later (it's not like we haven't already seen that...)! B) Core Syntax Review (back to top...) Here's a quick example of some things we should already be aware of:
var name = "Bruce"; var instrument = "drums"; var sticks = "sticks"; function description(thename, theax, theclubs) { return thename + " plays " + theax + " with " + theclubs; } console.log(description(name, instrument, sticks));
Here you've got variable (var), a basic function that includes some parameters, those parameters being "returned" using some concatenation and then we'll use this command to see it in GIT Bash: node play.js After running that, we'll see in our terminal: "Bruce plays drums with sticks." C) let, var and const (back to top...) 1) ECMA (back to top...) ECMAScript (or ES) is a trademarked scripting-language specification standardized by Ecma International in ECMA-262 and ISO/IEC 16262. It was created to standardize JavaScript, so as to foster multiple independent implementations. JavaScript has remained the best-known implementation of ECMAScript since the standard was first published, with other well-known implementations including JScript and ActionScript.[3] ECMAScript is commonly used for client-side scripting on the World Wide Web, and it is increasingly being used for writing server applications and services using Node.js. That's important because, on occasion, you'll hear "ES" in conjuction with JavaScript. For the most part, they're interchangeable in that "ES" is a standard that JavaScript abides by. From time to time, some improvements in JavaScript will be made as part of staying consistent with the ES standard. "let" is part of that effort. 2) let (back to top...) "let" allows you to avoid that situation that sometimes happens when you're changing the value of your "var" in the context of your application. Here's a great explanation from DEV: There's a weakness that comes with var. I'll use the example below to explain this.
var greeter = "hey hi"; var times = 4; if (times > 3) { var greeter = "say Hello instead"; } console.log(greeter) //"say Hello instead"
So, since times > 3 returns true, greeter is redefined to "say Hello instead". While this is not a problem if you knowingly want greeter to be redefined, it becomes a problem when you do not realize that a variable greeter has already been defined before. If you have use greeter in other parts of your code, you might be surprised at the output you might get. This might cause a lot of bugs in your code. This is why the let and const is necessary. let is preferred for variable declaration now. It's no surprise as it comes as an improvement to the var declarations. It also solves this problem that was raised in the last subheading. Let's consider why this is so. let is block scoped A block is chunk of code bounded by {}. A block lives in curly braces. Anything within curly braces is a block. So a variable declared in a block with the let is only available for use within that block. Let me explain this with an example.
let greeting = "say Hi"; let times = 4; if (times > 3) { let hello = "say Hello instead"; console.log(hello);//"say Hello instead" } console.log(hello) // hello is not defined
We see that using hello outside its block(the curly braces where it was defined) returns an error. This is because let variables are block scoped . let can be updated but not re-declared Just like var, a variable declared with let can be updated within its scope. Unlikevar, a let variable cannot be re-declared within its scope. So while this will work,
let greeting = "say Hi"; greeting = "say Hello instead"; // this will return an error
let greeting = "say Hi"; let greeting = "say Hello instead"; //error: Identifier 'greeting' has already been declared
However, if the same variable is defined in different scopes, there will be no error.
let greeting = "say Hi"; if (true) { let greeting = "say Hello instead"; console.log(greeting);//"say Hello instead" } console.log(greeting);//"say Hi"
Why is there no error? This is because both instances are treated as different variables since they have different scopes. This fact makes let a better choice than var. When using let, you don't have to bother if you have used a name for a variable before as a variable exists only within its scope. Also, since a variable cannot be declared more than once within a scope, then the problem discussed earlier that occurs with var does not occur. 3) const (back to top...) In those situations where you know your variable isn't going to change for nothing, no way, no how - you would use "const." Like this: const name="Bruce"; "name" will not change at all throughout the entire application. D) Arrow Functions (back to top...) 1) Anonymous vs Named Functions (back to top...) To convert the function that we wrote a moment ago using the Arrow Function approach, we start by referencing the function as a constant. So we started with this: var name = "Bruce"; var instrument = "drums"; var sticks = "sticks"; function description(thename, theax, theclubs) { return thename + " plays " + theax + " with " + theclubs; } console.log(description(name, instrument, sticks)); At this point, the "description" function is "declared" using "description" like what you see with console.log(description(name, instrument, sticks)); and the "description" function is technically called a "named" function. But you can also declare a function in the context of a variable. Like this: var name = "Bruce"; var instrument = "drums"; var sticks = "sticks"; const summarizeUser=function (thename, theax, theclubs) { we're now packaging our function as a constant. The function itself is now considered an "anonymous" function because it doesn't have a specific name return thename + " plays " + theax + " with " + theclubs; } console.log(summarizeUser(name, instrument, sticks)); ...we're going to get the same result. The only difference is that we're now calling our function as a value stored within a constant. And what was a "named" function is now referred to as an "anonymous" function. 2) "this" (back to top...) Now, by using the Arrow Function approach, you can write what we've got below in a manner that's a little shorter. This... const summarizeUser=function(thename, theax, theclubs) { ...becomes this: const summarizeUser=(thename, theax, theclubs) => { Now, apart from the fact that we were able to lose the word, "function," the other thing that the Arrow Function approach makes available is the "this" operator. Click here to access a good video that describes the "this" operator in some detail. The bottom line is that you can use "this" to call a function within the class that you're using "this" in. One thing that's a little distinctive about the way "this" is used in JavaScript as opposed to other languages is that, while it does refer to the functionality within the Class that it's being used in, it also refers to the dynamic that triggered the code to begin with. Here's the finished code with notes that's coming from the aforementioned video. Here's the JS:
class NameField { constructor(name) { const field = document.createElement('li'); field.textContent = name; const nameListHook = document.querySelector('#names'); nameListHook.appendChild(field); } } class NameGenerator { constructor() { const btn = document.querySelector('button'); this.names = ['Max', 'Manu', 'Anna']; this.currentName = 0; btn.addEventListener('click', () => { this.addName(); }); // Alternative: // btn.addEventListener('click', this.addName.bind(this)); } addName() { console.log(this); const name = new NameField(this.names[this.currentName]); this.currentName++; if (this.currentName >= this.names.length) { this.currentName = 0; } } } const gen = new NameGenerator();
Here's the HTML...
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>JS & "this"</title> </head> <body> <button>Add Name</button> <ul id="names"></ul> <script src="app.js"></script> </body> </html>


-> This is what puts the key in the ignition and gets the motor running. -> Here's the Class that's being instantiated and because you've got a constructor in place, that piece of the code is going to fire immediately -> const btn = document.querySelector('button'); this is looking through the document and grabbing the fist element that matches "button" -> this.names is the syntax that we would use to create the equivalent to a property. "names" is now available throughout the Class as you can see in the "addName" method -> here we're using the arrow operator and it's especially utilitarian in that we're using it as a way to afford us the opportunity to use the "this" operator in order to streamline the code that you see commented out. Take a look at what is written below... In the video, this is what was originally tried: btn.addEventListener('click', this.addName; The logic was that we could invoke the "addName" method by simply prefacing it with "this." After all, isn't that what "this" is supposed to do? The problem is that "this" is not only looking within the Class that it's positioned in, it's also going by the element that triggered the function in question. In this case, it's looking at the "btn." Because "addName" is not part of the "btn" dynamic, it throws and error. To get around that, you use the "bind" element. It's going to look like this: btn.addEventListener('click', this.addName.bind(this)); By using this, we're telling JavaScript to not look for "addName" in the context of the button, but rather to look for it in the context of the Class. Now, we're good to go. Here is where the Arrow Function comes to bear. Instead of writing btn.addEventListener('click', this.addName.bind(this));, we can write this: btn.addEventListener('click', () => { this.addName(); }); The Arrow Function gives us the chance to tell JavaScript to assume the scope of the button command to include the NameGenerator Class rather than just the button itself. 3) Some Shorthand (back to top...) This works: const add = (a, b) => { return a + b; }; If you've got a function that's a mere, one-line-return kind of dynamic, you can simply do this: const add = (a,b) => a + b; If I don't have any more than one argument, I don't need the paranthesis: const add = a => a + 1; ...and, in the event that I don't have any arguments at all, I can just have empty parenthesis and I'm good to go... const randomAdd = () => 1 + 2; BTW: To make "randomAdd" fire, you would write this in your GIT Bash: console.log(randomAdd()); E) Working with Objects, Properties and Methods (back to top...) Here we go: 1) Objects (back to top...) const person = { // "person" is an object name: 'Bruce', // key value pairs are called "properties" or "fields" of the object age: '55', greet () { // you can also have an anonymous function as a property of the object (note the way the syntax is notated console.log('Hi, I am ' + this.name +' and I am ' + ths.age); } person.greet(); } The above will output: Hi, I am Bruce and I am 55 a) Arrays (back to top...) An object can also be an array... const hobbies = ["drums", "gigs"]; for (let x of hobbies) { console.log(x); } This will output: drums gigs One thing that's kind of interesting is the number of possibilities that open up when you're dealing with arrays. When you add a "dot" at the end of your "hobbies" object, you get a display that shows you several options (see image to the right). Let's take a look at "map." map "map" allows you to edit the way in which the array is displayed. Check it out: console.log( hobbies.map(hobby => { return "Hobby: " + hobby; }) ); console.log(hobbies); This will output: [ 'Hobby: drums', 'Hobby: gigs' ] [ 'drums', 'gigs' ] And because of the options we have available to us as "shorthand" with the arrow function, we can get the same output using this: console.log(hobbies.map(hobby => "Hobby: " + hobby)); push While a "const" is typically something that doesn't change, because it's an array and therefore technically what is referred to as a "reference type," you can change the elements within the array without violating the "const" dynamic. So, if I do this: const hobbies = ["drums", "gigs"]; hobbies.push("cuts"); console.log(hobbies); I get this: [ 'drums', 'gigs', 'cuts' ] This is an important concept because it underscores the difference between "primative values" and "reference types." A "reference type" is a piece of code that is pointing to something. in other words, it represents a digital address of some kind of object. The "const" in the above example, technically, hasn't changed because it's an address as opposed to a legitimate value. F) Understanding Spread and Rest Operators
"Immutability" is that property of an array where you're never simply adding something to an array, rather you're constantly replacing the array with a copy plus whatever additions / changes you're making to it. In other words, the array is "immutable."
1) Spread (back to top...) "Spread" is an operator that you use to add elements to an array by copying the existing array and then adding an element to it. Take a look: a) slice (back to top...) const copiedArray = hobbies.slice(); console.log(copiedArray); When I run "node.js," I get my copied array: C:\wamp\www\adm\node>node play.js [ 'drums', 'gigs' ] "slice" copies the array and you can also pass arguments into it to limit the range of elements within the array you want to copy. b) spread (back to top...) If you were to do this: const copiedArray = [hobbies]; console.log(copiedArray) We would get this: C:\wamp\www\adm\node>node play.js [ [ 'drums', 'gigs' ] ] You get what's called a "nested array." It's an array within an array. While that might be helpful to use at some point, it's not what we're looking for now. However, if we add the "spread" operator, we're telling the system to take all of the elements within the object we're getting ready to "spread" and add those to the object that surrounds what it is that we're spreading. So, if we do this: const copiedArray=[...hobbies]; console.log(copiedArray); We get: C:\wamp\www\adm\node>node play.js [ 'drums', 'gigs' ] Perfect! And what you can do with Arrays, you can also do with Objects. Here's your "person object: const person = { name: "Bruce", age: 55, greet() { console.log("Hi, I am " + this.name); } }; We can use the "..." operator to extract that object and distribute its elements to the object that our "spread" operator is attached to. Just be sure to use "{}" instead of the square brackets. So, it will look like this: const copiedPerson = { ...person }; console.log(copiedPerson); ...and that will render: C:\wamp\www\adm\node>node play.js { name: 'Bruce', age: 55, greet: [Function: greet] } 2) Rest (back to top...) "Rest" is similiar to "spread" only in reverse. Take for example this situation: We're going to use an arrow function to return an array. const CFA = (var1, var2, var3) => { return [var1, var2, var3]; }; console.log(CFA("sandwich", "friends", "Coke")); When we run that in our command prompt, we get this: C:\wamp\www\adm\node>node play.js [ 'sandwich', 'friends', 'Coke' ] This, however, is limiting because we can't add any elements to that array on the fly. For example, we couldn't do this: console.log(CFA("sandwich", "fries", "Coke", "cookie")); It wouldn't return an error, but we would get the same result. By using the "Rest" operator though, we've got access to some more flexibility. Watch... Instead of const CFA=(var1, var2, var3)), we do const CFA=(...vars). The whole syntax looks like this: const CFA = (...vars) => { return vars; }; Now, when I pass in another property into the array like what I did before, I get the entire array... C:\wamp\www\adm\node>node play.js [ 'sandwich', 'frieds', 'Coke', 'cookie' ] G) Destructuring (back to top...)
const person = { name: "Bruce", age: 55, greet() { console.log("Hi, I am " + this.name); } }; const printName = personData => { console.log(personData.name); }; printName(person);
In the above code, you're passing the entire "person" object into the "printName" function. Remember, an object can be a function as well, and that's exactly what you have with the "printName" object in that you've got an arrow function called "personData." In this example, you're pulling the "name" property from the "person" array and printing it. That's one way to do it. The other way to do it is by using the "destructuring" syntax which looks like this: const printName = ({ name }) => { console.log(name); }; } By using the curly braces, you're telling the system to "destructure" the incoming data and pull out the "name" property. Another way in which "destructuring" can be used it like so: const { name, age } = person; console.log(name, age); You can also "destructure" an array like this: const hobbies = ["drums", "gigs"]; const [hobby1, hobby2] = hobbies; console.log(hobby1); This will return "drums." H) Async Code and Promises (back to top...) 1) Asynchronous vs Synchonous (back to top...) Let's start with a little review on what "Asynchronous" and "Synchronous" code is. If I've got this: setTimeout(( ) => { console.log('Timer is done!'); }, 1); console.log('Hello'); console.log('Hi'); The first block of code is considered "asynchronous" code because it doesn't happen right away. The second block is "synchronous" because it happens immediately. If you were to run this, you would get:
node play.js Hello Hi Timer is done!
The fact that "setTimeout" is first, because there's a delay of 1 millisecond, it will get fired after the other two lines have run. So... Asynchronous Code - means that there's a delay, it doesn't happen right away Synchronous Code - happens immediately 2) callback (back to top...) Also, a "callback" is a function that is executed within another function. You see it a lot in JQuery. For example, this: $('#button').click(function() { alert"Michelle"); }); The function with the "button" code is technically a "callback" in that it's a function within a function. As far as the way it's being used here, this example does a pretty good job of unravelling the mystery... function doHomework(subject, callback) { alert(`Starting my ${subject} homework.`); callback(); } doHomework('math', function() { alert('Finished my homework'); }); In this example, this: function() { alert('Finished my homework'); ...is what's being recognized in the system in the context of the "callback" variable. So, you get "Starting my math homework" which is the first part of the "doHomework" function. Then it moves to the "callback" which is the function function() { alert('Finished my homework');. 3) constructor (back to top...) "Constructors" is a JavaScript device that allows you create many objects of the same type. Click here for more information. 4) Promises (back to top...) Constructors constitute something you want to be aware of because of the way they facilitate the "promise" dynamic. "Promises" represent a more verbose / easier way of triggering callbacks. Thing is, you rarely have to write them on your own as much as they are happening behind the scenes in the context of pre-packaged blocks of syntax. For now, just know how they look:
const fetchData = () => { const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve("Done!"); }, 1500); }); return promise; }; setTimeout(() => { console.log("Timer is done!"); fetchData().then(text => { console.log(text); }); }, 2000); console.log("Hello"); console.log("Hi");
I) Template Literals (back to top...) One other feature, we'll use from time to time are template literals: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals It's a different way of writing strings. Instead of using double or single quotation marks: 'A String' or "Another string" you can use backticks (`) `Another way of writing strings` Now why would we use that way of creating strings? With that syntax, you can dynamically add data into a string like this: const name = "Max"; const age = 29; console.log(`My name is ${name} and I am ${age} years old.`); This is of course shorter and easier to read than the "old" way of concatenating strings: const name = "Max"; const age = 29; console.log("My name is " + name + " and I am " + age + " years old."); This section is designed to go over the rudimentary aspects of Node and the web in general. A) How the Web Works (back to top...) This is pretty easy. I'm including two graphics that pretty much make the point...
B) Set up a Server (back to top...) 1) Core Modules (back to top...) Node comes with a group of Core Modules by default. They are: As far as setting up your server, you've got more than one option. 2) Setup Code (back to top...) This is "Event Driven Architecture," by the way. This first example uses a "named" function: const http = require('http'); function rqListener(req, res) { } http.createServer(rqListener); You can also do it this way - using an anonymous function: const http = require('http'); http.createServer(function(req, res)); Finally, you can do it this, using an arrow function: const http = require("http"); const server = http.createServer((req, res) => { console.log(req); }); server.listen(3000); C) Event Loop (back to top...) The thing you want to be aware of is that, because of the way the Listener is set up, you never quit "running" the server. That anomoly is called an, "Event Loop." Take a look:
The "event" of the server is never terminated. From that standpoint, it's called an "Event Loop." And that's a good thing! You don't want that process to stop. To terminate your session, just hit "Ctrl-C." D) Sending Responses (back to top...) You can adjust the code you used to set up your server so that you can see some responses. Check it out:
const http = require("http"); const server = http.createServer((req, res) => { console.log(req.url, req.method, req.headers); // here's where you're logging some activity }); server.listen(3000);
Here's the response:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/project $ node app.js / GET { host: 'localhost:3000', // here's the method and the URL. The rest of the info is header info... connection: 'keep-alive', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Ge cko) Chrome/71.0.3578.98 Safari/537.36', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng, */*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', cookie: '_ga=GA1.1.2029811492.1513464095' }
If you wanted to send some html code and some text, it would look like this:
const http = require("http"); const server = http.createServer((req, res) => { console.log(req.url, req.method, req.headers); res.setHeader("Content-Type", "text/html"); "res" means "response" res.write("<html>"); res.write("<head><title>Test Page</title>"); res.write("<body><h1>I've got to fix that stupid NOMAS site!</h1></body>"); res.write("</html>"); res.end(); //this is important! }); server.listen(3000);
This will give you:

I've got fix that stupid NOMAS site!

E) Request and Response Headers (back to top...) Part of what we wrote a moment ago included "headers." While that may be a familiar term, there are some things about that we'll get into later. For now, here's an online resource that lists everything we'll ever need as far as "Request and Response Headers." F) Routing (back to top...) What we're going to do in this example is use an element in the URL to dictate the flow and functionality of our page. Check it out:
const http = require("http"); const server = http.createServer((req, res) => { const url = req.url; // set up a const ->url if (url === "/") { // if url equals nothing, then fire the following code... res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write( '<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>' ); // we've got a little form, here...! res.write("</html>"); return res.end(); // *see the notes below... } res.setHeader("Content", "text/html"); res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); }); server.listen(3000);
*As a rule, you don't include any code after "res.end." In this case, despited the "IF" clause, our code would continue if we just wrote "res.end." But by prefacing it with "return," then the code stops at that point as it should. G) Redirecting Requests (back to top...) This time we're going to post the form that we wrote above and redirect the result based on the form having been posted. Check it out...
const http = require("http"); const fs = require("fs"); const server = http.createServer((req, res) => { const url = req.url; const method = req.method; // set up a new const called "method" so we can track whether or not something's been posted if (url === "/") { res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write( '<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>' //here's your form ); res.write("</html>"); return res.end(); } if (url === "/message" && method === "POST") { // if the URL is "message," which is the route of the posted form, and the method equals "POST," then... fs.writeFileSync("message.txt", "DUMMY"); // write a new file called, "message.txt" res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); // remember to return the "res.end" dynamic so the code doesn't continue to run } res.setHeader("Content", "text/html"); res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); }); server.listen(3000);
H) Parsing Requests (back to top...) When you send information via a form, Node is sending it as a stream of data rather than as a string or something similar. Below is a graphic that shows how it's being sent and processed.
In order to access the incoming stream, we do this:
const http = require("http"); const fs = require("fs"); const server = http.createServer((req, res) => { const url = req.url; const method = req.method; if (url === "/") { res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write( '<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>' ); res.write("</html>"); return res.end(); } if (url === "/message" && method === "POST") { const body = []; req.on("data", chunk => { console.log(chunk); body.push(chunk); }); req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); console.log(parsedBody); }); fs.writeFileSync("message.txt", "DUMMY"); res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); } res.setHeader("Content", "text/html"); res.write("<htm>l"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); }); server.listen(3000);
The code above produces what you see below.
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node $ node play.js <Buffer 6d 65 73 73 61 67 65 3d 62 72 69 6e 67 2b 69 74> message=bring+it
To take this apart, let's take a look at the syntax above that's in bold: const body = []; req.on("data", chunk => { console.log(chunk); body.push(chunk); }); req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); console.log(parsedBody); }); The way this breaks down is relatively simple. You've got some incoming data based on your route and your method: if (url === "/message" && method === "POST") {. Before you got write or create a file, we're going to assert a little code so we can access the incoming data stream. We're going to start by "registering" an Event Listener. An Event Listener is basically a piece of code that's triggered by a certain event. In this case, "on" is a method that we have available to use thanks to the server we created earlier (const server = http.createServer((req, res) => {).
BTW: We're not altering our "body" constant as far as reassigning it. In other words, we're not doing something like: body = "hello"; "body" was originally defined as an empty array. While we can add values to that array, we can't redefine the "body"as the object. We can, however, edit its value.
req.on("data", (chunk) => { //using an ES6 arrow function The "on" method expects two arguments. The first is the name of the event itself which, in this case, is "data..." The second argument is what we do after that event has been "heard." In this instance, the second argument is actually a function. By default, the function is going to be looking for a "chunk" as per the way Node operates in this context, hence the "chunk" in the opening parenthesis. For the function, we're going to start by instantiating a new constant called "body" and then we're pushing const body=[]; - the body is going to be an empty array body.push(chunk) - we're pushing our "chunk" of data into the empty array. req.on('end', () => { - Now, we've got register yet another new Listener to handle what amounts to the end result. Again, we'll grab the "on" method which will expect two arguments. The first one is "end." It's going to "listen" for what is the end of the incoming request. const parsedBody = Buffer.concat(body).toString(); - at this point, we can rely on the fact that the "body" constant contains all of the info that corresponds to the incoming request and we're going to use the "Buffer" object to combine it into a single string. Alright, now... Let's take the incoming string and write that to the message.txt file. To do that is relatively easy. The code is going to look like this: req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split('=')[1]; fs.writeFileSync("message.txt", message); }); This has already been discussed. We're using the "Buffer" object to take all the pieces that have been collected and turn them into a cohesive string. "message" is going to be what holds the portion of the "parsedBody" array that coincides with the "message" value. The "parsedBody" value is going to be a series of "key / value" pairs. "message" is going to be the value to the right of equal sign that's in the first position in the array. We're now using a line from the original code, but we're placing it within the "end" function so that we're now writing the "message" constant value to the "message.txt" file. I) Understanding Event Driven Code Execution (back to top...) JavaScript is a "single threaded, asynchronous language." A "thread" is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. So, in other words, you're dealing with one systemic conversation at a time. However... While it may be one conversation, it's very much like an order in a restaurant kitchen. It's one order, but you're engaging several processes simultaneously. If someone orders a Bacon & Cheese Hamburger, you're toasting the bun, grilling the meat, cooking the bacon - all at the same time. Now, imagine a situation where everything about the preparation of that Back & Chesse Hamburger was being done in order. So, I start with grilling the meat and only after I'm done grilling the meat do I toast the bun. And only after I'm done toasting the bun to begin to fry the bacon. That kind of approach would be called a "Synchonous" process. Everything is being done in a specific order. "Asynchronous," on the other hand, means I'm multi-tasking. So, I'm grilling the meat, frying the bacon and toasting the bun all at the same time. It's a very efficient way of getting things done, but... ...it can lead to trouble if one of your processes outruns another that it's dependent on. If my meat is done cooking before the bun is ready, then I've got no place to put my beef patty. This is why you have to be careful when you're writing JavaScript because, while it is capable of running asynchronous code, it can be very unforgiving if you're not structuring your code so it's not triggering processes that require other functions to conclude before they start turning over. That said, take a look at what we just wrote:
if (url === "/message" && method === "POST") { // everything in grey gets triggered with the "Post" dynamic const body = []; req.on("data", chunk => { console.log(chunk); body.push(chunk); }); req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split("=")[1]; fs.writeFileSync("message.txt", message); }); res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); } res.setHeader("Content", "text/html"); res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); });
The part that's in bold is potentially problematic becauseTo solve that problem, the code that's in bold should be moved up into the EventListener code so it would look like this: req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split("=")[1]; fs.writeFileSync("message.txt", message); res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); }); } Problem is, though, that the EventListener is going to be registered, but the processing of the code in general will continue and we'll get the "My First Page" page. Moreover, you'll get an error message because the "res.SetHeader" code has fired and already written the headers. So, by the time Node goes back to process the "end" object, it can't and you'll get this:
$ node play.js <Buffer 6d 65 73 73 61 67 65 3d 62 72 69 6e 67 2b 69 74> _http_outgoing.js:470 throw new ERR_HTTP_HEADERS_SENT('set'); ^ Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the cli ent at ServerResponse.setHeader (_http_outgoing.js:470:11) at IncomingMessage.req.on (C:\wamp\www\adm\node\play.js:27:11) at IncomingMessage.emit (events.js:182:13) at endReadableNT (_stream_readable.js:1094:12) at process._tickCallback (internal/process/next_tick.js:63:19)
To do this correctly, you leave the order of the code intact, but you change the "fs.writeFileSync" line and convert it to a function that DOES NOT fire synchronously. Instead it fires as a proper call back.

Terms and Concepts...

Runtime - when a program is running Heap - or Binary Heaps, is the systemic approach that JavaScript takes in storing information (see above graphic) JavaScript Engine - it's a computer program that executes JavaScript code Compiled vs Interpreted Code - Computer code is often a collection of characters that are unique in that they're written in a particular programming language. There are two types of software: Application Software and System Software. An example of System Software would be the Windows OS or Linux. These "machines" understand numbers and that's it. Everything you want these Operating Systems to do has to be a command that's been interpreted into some kind of digit. Application Software would be applications like Word or Photoshop. Computer Operating System is going to be a combination of System Software and Application Software (see graphic to the right). Compiler - a Compiler is a computer program that transforms computer code written in one programming language (the source language) into another programming language (the target language). There are two types of computer programs. They're either going to be "compiled" or "interpreted." A "compiled" program will result in a program that is capable of performing some task. An "interpreted" program will result in something actually being done (see both Wikipedia and Indiana University). JavaScript is an "interpreted" code. Single Thread - one Call Stack. It's doing one thing at a time. Client Side - a computer action that's taking place on your user's (client's) computer. "Server side," conversely, means that it's a piece of functionality that's occuring on the web server. Non-Blocking - this term refers to the way in which Node.js is set up in such a way where it can move things from the Call Stack to other parts of the Event Loop (Task Cue, Worker Pool, etc) so it doesn't prevent the user's experience from getting slowed down by having to wait for a particular process to conclude
1) Event Loop (back to top...) In order to understand what an "Event Loop" is, the first thing we need to do is get a grip on some basic terms and concepts. There's a smattering of things in the callout box to the right, including the difference between "compiled" code and "interpreted" code. First, however, let's take a look at what the "Callstack" is. a) Call Stack (back to top...) The Callstack - is a record of where we're at in the program. One thing that's significant about the way JavaScript works is that it "assembles" all of what you're doing before it actually fires. For example, take a look at the graphic below:
If you look at the graphic above, you'll notice that the function being called is "printSquare(4)." So, that's added to the Call Stack at the very bottom, in that it's the first function being called. But within "printSquare," another function's being called which is "square," so that gets added to the Call Stack. Within "square," you're calling "multiply" so that is added to the stack resulting in a "stack" of functionality with "multiply" being on the very top. The "Call Stack" operates according to the LIFO dynamic which stands for, "Last In, First Out." That makes sense and it's a good thing because that's the only way in which console.log(squared) is going to display an accurate value. You have to multiply 4 * 4 in order for square to return a legitimate value and then that gives printSquare something to print to the console. Cool! b) Blocking (back to top...) "Blocking" is a term that's used to describe a part of the stack that tends to run slow. For example, a "network request" can be slow. That will slow down the entire process in that it will "clog" your call stack and prevent other functions from occuring. c) Concurrency & Web API's (back to top...) "Concurrency" means that multiple things are happening simultaneously. That becomes relevant to our discussion because, while JavaScript is a single thread, syncronous language, because of WebAPI's, it can function as something that handles more than one event asyncronously. i) Web APIs (back to top...) Web APIs are pieces of functionality that exist in the context of your web browser. Things like the DOM, AJAX and setTimeout. These are the "extra" pieces of the puzzle that JavaScript makes use of in order to deliver asyncronous processes (see diagram to the right). In the diagram you see below, you've got a setTimeout function that's going to be added to the stack, but then moved over to the API section where it will do its thing. Meanwhile, the Stack continues to function like it normally does in that it keeps moving things to the top and then discarding them as their respective functions are accomplished. BTW: What you see below is exactly the same with Node, only instead of Web API's, they're replaced with C++ API's which give you the same functionality, but without the web browser. That said, let's take a look at how this works...
Here's our starting point. Notice the various "pieces" of the puzzle...
The first piece of our program runs and is added to the Stack. In our Console we see, "Hi." Once that's concluded, it's removed from the Stack and we proceed. By the way, the LIFO dynamic isn't really relevant here because we're not calling a function within a function. That's where the LIFO hierarchy would apply. Here's it's pretty cut and dry. We're in and we're out. Next we call our setTimeout function. This is where our Web API's kick in. It's funneled over to the Web API section where it just sits and does it's thing. Meanwhile...
setTimeout is removed from the stack and console.log makes its way into the stack and it does its thing. As soon as its done, it's removed and...
You're left with the setTimeout still running. Once it's done, however, it can't just assert itself back into the stack without the risk of disturbing what's in the stack and creating a systemic mess. So, once that setTimeout is done...
The setTimeout goes to the "Task Cue."
Here is where we encounter the "Event Loop." The purpose of the "Event Loop" is to serve as a liason between the Call Stack and the Task Cue. If the Call Stack is empty and there's something in the Task Cue, the Event Loop will move it to the Call Stack. It's that simple.
The HyperText Transfer Protocol (HTTP) 302 Found redirect status response code indicates that the resource requested has been temporarily moved to the URL given by the Location header. A browser redirects to this page but search engines don't update their links to the resource (in 'SEO-speak', it is said that the 'link-juice' is not sent to the new URL
A "Call Back" is a function that, by definition, is going to be moved into the Task Cue and made to wait until the Call Stack is empty. It's not necessarily "wrong" to think of it as something that is attached to a setTimeout with a value of "0." After all, it's the setTimeout dynamic that's going to move it into the Web API box. But, you don't necessarily have to think about it that way, as long as you're recognizing the fact that the "Call Back" is being placed into the "Task Cue" and made to wait until the Stack has been depleted. You can see the video tutorial that breaks all this down by clicking here. 2) The "better code..." (back to top...)
if (url === "/message" && method === "POST") { // everything in grey gets triggered with the "Post" dynamic const body = []; req.on("data", chunk => { console.log(chunk); body.push(chunk); }); req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split("=")[1]; fs.writeFileSync("message.txt", message); }); res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); } res.setHeader("Content", "text/html"); res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); });
We're going to take the code that you see in grey and rewrite it like this:
if (url === "/message" && method === "POST") { // everything in grey gets triggered with the "Post" dynamic const body = []; req.on("data", chunk => { console.log(chunk); body.push(chunk); }); return req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split("=")[1]; fs.writeFileSync("message.txt", message); res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); }); } res.setHeader("Content", "text/html"); res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); });
"return" acts as a "die," in some ways. In this case, it will force the Call Stack to empty itself and conclude whatever business it was doing. "All done!" That's what "return is doing in this instance. Then to make things wrap up with a nice, neat little bow, we put the res.statusCode = 302 etc. within the IF clause so the page goes back to its orginal look and feel. J) Blocking and Non-Blocking Code / writeFileSync vs writeFile (back to top...) Take a look at this line of code: fs.writeFileSync("message.txt", message); "Sync" stands for "syncronous." Bottom line: The next line of code won't fire until the "File" has been written. This isn't a problem in this instance, because we're writing a very shot file. If, on the other hand, we were writing a massive file, we wouldn't want this line to "block" the rest of the code from firing. Instead, we're going to use "writeFile." This takes three arguments, the third being an error. So, it will look like this: fs.writeFile('message.text', message, err => { res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); }); Since we're not planning on an error, we're just going to let that object trigger the response we're planning on, so it will look like this: fs.writeFile("message.txt", message, err => { res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); }); Perfect! K) Behind the Scenes (back to top...) 1) Worker Pool (back to top...) Having already discussed how JavaScript will offload Call Backs into the Task Cue which is then directed by the Event Loop to re-enter the Call Stack when appropriate, let's now consider the "Worker Pool." The diagram below shows how those tasks that involve some truly "heavy lifting" are routed to what's called the "Worker Pool." It's not that different from the Task Cue, but in those cases where you've got some time consuming tasks that might otherwise affect the user's experience, the Worker Pool is the place where those tasks are performed. Take a look:
2) Serious Event Loop (back to top...) The Event Loop is Node is a little more verbiose than what was discussed earlier. Take a look:
L) Using the Node Modules System (back to top...) Just like you can use "require" and "include" in the context of a PHP environment and streamline your code so it's easier to follow, you can do the same thing in Node. Here's what you have now:
const http = require("http"); //everything that 's in orange, you're copying and pasting into a new file called "routes.js" const fs = require("fs"); const server = http.createServer((req, res) => { const url = req.url; const method = req.method; if (url === "/") { res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write( '<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>' ); res.write("</html>"); return res.end(); } if (url === "/message" && method === "POST") { const body = []; req.on("data", chunk => { console.log(chunk); body.push(chunk); }); return req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split("=")[1]; fs.writeFile("message.txt", message, err => { res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); }); }); } res.setHeader("Content", "text/html"); res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); }); server.listen(3000);
Here's the way your "new" play.js looks now:
const http = require("http"); const routes = require("./routes"); // here's where you're importing "routes" into the flow... const server = http.createServer(routes); // here's where you're implementing the imported code server.listen(3000);
Here's how the "routes.js" code looks:
const fs = require("fs"); const requestHandler = (req, res) => { // this is the way in which you're "wrapping" all of the code you've brought over from "play.js" in a const called "requestHandler." Notice the "request" and the "response" objects... const url = req.url; const method = req.method; if (url === "/") { res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write( '<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>' ); res.write("</html>"); return res.end(); } if (url === "/message" && method === "POST") { const body = []; req.on("data", chunk => { console.log(chunk); body.push(chunk); }); return req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); const message = parsedBody.split("=")[1]; fs.writeFile("message.txt", message, err => { res.statusCode = 302; res.setHeader("Location", "/"); return res.end(); }); }); } res.setHeader("Content", "text/html"); res.write("<html"); res.write("<head><title>My First Page</title></head>"); res.write("<body><h1>Yo, dog!</h1></body>"); res.write("</html>"); res.end(); }; // here's the "end" of your "responseHandler" const module.exports = requestHandler; // here's how you're making Node aware of the "requestHandler" export that can be implemented into other files
You can also export "requestHandler" by doing this... module.exports = { handler: requestHandler, someText: 'Pucketts is awesome!' } Now, when you go to import "requestHandler," you'll do that by writing: const server = http.createServer(routes.handler); ...and you can write to the console, by writing: console.log(routes.someText; You can do the same thing by writing: module.exports.handler=requestHandler; module.exports.someText='Some hard code'; ...and even: exports.handler=requestHandler; exports.someText='Some hard code'; M) Homework (back to top...) Homework...!
const http = require("http"); const server = http.createServer((req, res) => { const url = req.url; const method = req.method; if (url === "/") { res.setHeader("Content-Type", "text/html"); res.write("<html>"); res.write("<head><title>Assignment</title>"); res.write( "<body><form action='/users' method='POST'><input type='text' name='message'><button type='submit'>Send</button></form></body>" ); res.write("</html>"); res.end(); } if (url === "/users" && method === "POST") { const body = []; req.on("data", chunk => { body.push(chunk); }); req.on("end", () => { const parsedBody = Buffer.concat(body).toString(); console.log(parsedBody); }); res.setHeader("Content-Type", "text/html"); res.write("<html>"); res.write("<head><title>Assignment</title>"); res.write("<body>Form has been submitted!</body>"); res.write("</html>"); res.end(); } }); server.listen(3000);
A) NPM Scripts (back to top...) We've worked with this before. "NPM" stands for "Node Package Manager." It comes with Node, so you've already got it. With NPM, you can exploit some shortcuts that allow you to be more efficient in the way you write your code. The most common shortcut is "start." 1) npm init (back to top...) Start by typing "npm init in your console. Be sure you're in the correct directory! After you do that, you'll be given several questions to answer. Take a look:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node $ npm init // here's your command This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See `npm help json` for definitive documentation on these fields and exactly what they do. Use `npm install ` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. //here are all your questions package name: (node) version: (1.0.0) description: node course entry point: (play.js) // this one is important in that you want to correctly identify the file that's used to kick off your code test command: git repository: keywords: author: Bruce Gust license: (ISC) About to write to C:\wamp\www\adm\node\package.json: { // here's the JSON file that's about to be written into your director "name": "node", "version": "1.0.0", "description": "node course", "main": "\u001b[D\u001b[D\u001b[D\u001b[Dplay.js)", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Bruce Gust", "license": "ISC" } Is this OK? (yes)
Once you click on "OK," a new file appears in your home directory. It's the "package.json" file. Now, watch this:
{ "name": "node", "version": "1.0.0", "description": "node course", "main": "\u001b[D\u001b[D\u001b[D\u001b[Dplay.js)", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node play.js" // you can write a little shortcut, here, so instead of having to type, "node play.js" every time, now you can just type npm start }, "author": "Bruce Gust", "license": "ISC" }
You can write your own custom shortcuts, but be aware that what you see above works because "start" is a special word. Hence, they system understands, npm start. But, if under "scripts," you had this: "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node play.js", "start-server":"node play.js" } If you were to try and type "npm start-server" in your console, you would get an error. For custom scripts, you have to write, "npm run start-server." Then everything is gold! B) Third Party Packages (back to top...) Rarely will you not have a number of 3rd party packages included in your application just because of the options and the uitlities that they bring to the table. They all live on the "NPM" website and you automatically access that resource by simply typing npm install 1) nodemon (back to top...) "nodemon" aids in streamlining your developmental processes by eliminating the need to manually restart the server every time you make a change to your code. To install it, all you need to do is type npm install nodemon. However... a) --save-dev (back to top...) Some packages, like "nodemon," are only relevant to your "dev" process. You don't want them included as part of your "prod" version. In that instance, you would type npm install --save -dev. b) -g (back to top...) A third option is to type npm install nodemon -g. That would result in "nodemon" being installed globally. After you type npm install nodemon --save -dev, you'll see all of the dependencies get installed in a new directory called, "node_modules." In addition, you'll see a new line on your "package.json" file that looks like this: "devDependencies": { "nodemon": "^1.18.9" } The "^" character means that if you were to run npm install, that character says that NPM would automatically install the newest version if you were to reinstall it. BTW: "package-lock.json" is a JSON file that lists your modules / dependencies according to their current version. This comes in handy when you hand your project off to another developer and they need to be using the version of a certain module that you initially used and not the most current one. 2) Core Concepts (back to top...) This comes right from the tutorial. It's a list of terms and concepts that were covered in this section...
The last lectures contained important concepts about available Node.js features and how to unlock them. You can basically differentiate between: Global features: Keywords like const or function but also some global objects like process Core Node.js Modules: Examples would be the file-system module ("fs"), the path module ("path") or the Http module ("http") Third-party Modules: Installed via npm install - you can add any kind of feature to your app via this way Global features are always available, you don't need to import them into the files where you want to use them. Core Node.js Modules don't need to be installed (NO npm install is required) but you need to import them when you want to use features exposed by them. Example: const fs = require('fs'); You can now use the fs object exported by the "fs" module. Third-party Modules need to be installed (via npm install in the project folder) AND imported. Example (which you don't need to understand yet - we'll cover this later in the course): // In terminal/ command prompt npm install --save express-session // In code file (e.g. app.js) const sessions = require('express-session');
3) Using Nodemon (back to top...) Now that you've got Nodemon installed, to use it we're just going to edit our package.json file so it's automatically triggered every time we go to start our app. Right now, our package.json file looks like this:
{ "name": "node", "version": "1.0.0", "description": "node course", "main": "\u001b[D\u001b[D\u001b[D\u001b[Dplay.js)", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node play.js" }, "author": "Bruce Gust", "license": "ISC", "devDependencies": { "nodemon": "^1.18.9" } }
We're going to change what's in blue to: "start": "nodemon play.js" Now, when we type npm start, the server will restart automatically. In addition, if we make any changes to our code, rather than having to restart the server manually, it will do so automatically. Not bad! As an aside, if you were try and run nodemon play.js from the terminal, you would get an error because "nodemon" isn't installed globally. Since it's installed only locally, you can only trigger it like what you've got above. This comes from the lecture and reinforces the whole concept of global vs local modules...
In the last lecture, we added nodemon as a local dependency to our project. The good thing about local dependencies is that you can share projects without the node_modules folder (where they are stored) and you can run npm install in a project to then re-create that node_modules folder. This allows you to share only your source code, hence reducing the size of the shared project vastly. The attached course code snippets also are shared in that way, hence you need to run npm install in the extracted packages to be able to run my code! I showed that nodemon app.js would not work in the terminal or command line because we don't use local dependencies there but global packages. You could install nodemon globally if you wanted (this is NOT required though - because we can just run it locally): npm install -g nodemon would do the trick. Specifically the -g flag ensures that the package gets added as a global package which you now can use anywhere on your machine, directly from inside the terminal or command prompt.
C) Finding and Fixing Errors (back to top...) You've got three kinds of errors. Syntax, Runtime and Logic. Most of these errors you're going to be able to identify in the context of the color coding and characters displayed in Visual Studio. You can also use the Visual Studio Debugger to investigate errors in your logic. Click here and here for more information on how to use the Visual Studio Debugger. A) Quick Review - Setup a New File (back to top...) Real quick, we're going to start a new file just for the sake of keeping things real. So, set up a new directory and then from there... 1) npm init (back to top...) This is going to set up your json file where it will list all of your dependencies etc. 2) nodemon (back to top...) Here, you're going to type npm install nodemon --save -dev That's going to set your nodemon dynamic so you don't always have to manually restart your server. 3) shortcuts (back to top...) Now, set up your shortcut for your "start" command. Instead of having to type node start app.js, with this all you'll do is type "npm start." To make that happen, go to your JSON file and type... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon app.js" }, B) Express Setup (back to top...) To set up Express, you're going to type this: npm install --save express. Once that's done, when you go to your "app.js" file, you'll do this:
const http = require("http"); const express = require("express"); const app = express(); const server = http.createServer(app); server.listen(3000);
BTW: If you were to hover over the "express" word in the the const express = require("express");, press "Cntrl-click," it will take you to the "index.d.ts" file. There you'll see how "Express" is being exported as export = e;. That's why you can execute it as a function. It's also a valide request handler, which is is why you can pass it into "http.createServer." It sets up a certain way it handles incoming requests. C) Middleware (back to top...) Express functions as a kind of middleware in that it's processing incoming requests and funneling it through some code that we would otherwise have to write. Thank God for Express. Thank God for Middleware. This is the way it's going to get set up. Here's your "app.js" file:
const http = require("http"); const express = require("express"); const app = express(); app.use((req, res, next) => { console.log("In the middleware"); }); const server = http.createServer(app); server.listen(3000);
"Middleware" is defined as "software that acts as a bridge between an operating system or database and applications, especially on a network." It's an apt description of "Express" in the way "Express" allows for the opportunity to compose a lot of functionality with minimal code. It's more than just a library, though. It's not like JQuery in that with Express, you have access to a technology that's serving as the bridge between two technologies. In this case, you're iteracting with the digital zone that exists between the web browser and your application. Specifically, the "request" and the "response" object.

For more information, click here.
instantiate the express paradigm put the express package into a constant called "app" "use" is a method available to us via Express. It expects three arguments: request, response and "next." "next" is important and you'll see why in a minute. If you put "In the middleware" like you see it above, you'll get "In the middleware" in the console. However, if you do this: app.use((req, res, next) => { console.log("In the middleware"); }); app.use((req, res, next) => { console.log("In the middleware again"); }); The only thing you're going to get in the console is "In the middleware." You won't get "In the middleware | In the middleware again." You have to include "next" like this: app.use((req, res, next) => { console.log("In the middleware"); next(); // allows the request to continue to the next middleware in line }); app.use((req, res, next) => { console.log("In the middleware again"); }); Now the server knows to be going through your code in a way that anticipates another round of functionality. If you don't call, "next," your code will die at that point. D) How Middleware Works (back to top...) Right here... app.use((req, res, next) => { res.send('<h1>Hello from Express</>'); console.log("In the middleware again"); Rather than having to do res.write... and manually write all of the HTML header info etc., we can use "res.send" and Express will do all of that header information for us. So, instead of having to document all of those "write" chunks, Express sets up those HTML headers for us. E) Behind the Scenes (back to top...) If you head out to the GIT Repository for Express and click on "lib" and then look at the "response.js" code, you'll see where these shortcuts and snippets are coming from. Do a search for "send(" and you'll see how we're able to pull off "res.send()" like we did earlier, as far as the headers being crafted for us. This is line #141 of the response.js file: switch (typeof chunk) { // string defaulting to html case 'string': if (!this.get('Content-Type')) { this.type('html'); } Another healthy piece of background information is on the "application.js" file located also in the "lib" directory. This is coming from line #609 and #610: http.createServer(app).listen(80); * https.createServer({ ... }, app).listen(443); This is what allows us to make some additional "cuts" to our "app.js" code. Now, instead of this: const server = http.createServer(app); server.listen(3000); ... we can do this: app.listen(); Plus, we can also get ride of "const http = require("http"); BOOM! F) Different Routes (back to top...) If you go out to expressjs.com, you can see the documentation that pertains to "path." What we're looking at is app.use([path,] callback [, callback...]). Based on that, if we did this: app.use('/', (req, res, next) ...the addition of '/', means that we're now "routing" our user to the syntax that immediately follows that code. This, however, is the default.
"/" applies to any route that starts with a slash. That, of course, is going to be every route. So, while it obviously is useful for the sake of empty route, be aware that without additional code, this is as far as your user will ever get!
To get to that place where we can handle other routes, we do this: const express = require("express"); const app = express(); app.use("/add-product", (req, res, next) => { console.log('The "Add Product" Page'); res.send('<h1>The "Add Product" Page!</h1>); // if you put your customary "next" here, this route would not be acknowledged }); app.use("/", (req, res, next) => { console.log("In the middleware again"); res.send("<h1>Hello from Express</h1>"); }); app.listen(3000); The reason this works the way that it does is because Express reads the code top to bottom. If you put a "next" in the first clause, the code would continue and the "add-product" dynamic wouldn't register. In the absence of the "next" dynamic, you've got a legitimate routing clause. By the way, if we wanted to have some dynamic in place that always ran, regardless of the route, then you would simply put this on the top of the stack: app.use("/", (req, res, next) => { console.log("Always running, my man!"); next(); }); As a whole, it would look like this:
const express = require("express"); const app = express(); app.use("/", (req, res, next) => { console.log("Always running, my man!"); // here's what we're going to run every time next(); }); app.use("/add-product", (req, res, next) => { console.log('The "Add Product" Page'); res.send('<h1>The "Add Product" Page</h1>'); }); app.use("/", (req, res, next) => { console.log("In the middleware again"); res.send("<h1>Hello from Express</h1>"); }); app.listen(3000);
Here's what our console is going to look like:
Always running, my man! The "Add Product" Page Always running, my man! In the middleware again
Notice that while you're sending a response only once, the fact that we're writing to console based on the presence of the "/" character in a way that is constant is what produces the redundancy of that piece of the code. G) Parsing Incoming Requests (back to top...) With this we're going to have a form that posts to a particular route and then parse the incoming data using Express. What was kind of convolluted before, is now pretty streamlined. Check it out:
const express = require("express"); const bodyParser = require("body-parser"); const app = express(); app.use(bodyParser.urlencoded()); app.use("/add-product", (req, res, next) => { res.send( '<form action="/product" method="Post"><input type="text" name="title"><button type="submit">Add Product</button></form>' ); }); app.use("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }); app.use("/", (req, res, next) => { res.send("<h1>Hello from Express</h1>"); }); app.listen(3000);
You're going to have to install a new Express third party package called "bodyParser." You're going to do that by writing $ npm install --save body-parser This is going to do everything that we had to write by hand earlier. BTW: When you're getting ready to use a third party package, the first thing you're doing code-wise is "importing" it and storing it in an object (const). app.use(bodyParser.urlencoded()); "registers" the middleware and calls the "urlencoded" function which is going to split, parse and produce the data coming in through our form Prior to importing the "bodyParser" package, console.log(req.body) threw an error. With bodyParser, we get title: 'Muscular Christianity' H) Limiting Middleware Execution to POST Requests (back to top...) Right now, our code will parse both POST and GET requests, which isn't always going to be healthy. No biggie. Just change, "use," to "post" and you're all set. What's great about this is that, while our "product" route is purely for the sake of processing our form, by using "post," a user could go to out that page and not be confronted with an error because the page was looking for either a GET or a POST request. By using "post," now it's not triggered for anything other than an incoming form. So, while before we had: app.use("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }); ...now we've got: app.post("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }); I) Using Express Router (back to top...) To do the equivalent to "require" like you would in PHP to streamline your code, you're going to handle it like this: This is your code now:
const express = require("express"); const bodyParser = require("body-parser"); const app = express(); app.use(bodyParser.urlencoded()); app.use("/add-product", (req, res, next) => { res.send( '<form action="/product" method="Post"><input type="text" name="title"><button type="submit">Add Product</button></form>' ); }); app.use("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }); app.use("/", (req, res, next) => { res.send("<h1>Hello from Express</h1>"); }); app.listen(3000);
What you have in yellow is going to be your "admin.js" file and what you have in red is going to be your "shop.js" file. To pull this off, you're going to need the "routes" package and you'll also need to be sensitive to the order with which you place your various elements. First, let's take a look at what the "new" app.js file is going to look like:
const express = require("express"); const bodyParser = require("body-parser"); const app = express(); const adminRoutes = require("./routes/admin"); const shopRoutes = require("./routes/shop"); app.use(bodyParser.urlencoded()); app.use(adminRoutes); app.use(shopRoutes); app.listen(3000);
- you're going to create an object "adminRoutes" to hold the code coming from your new "admin.js" file. Notice you don't have to specify ".js." Express already assumes that. - here's your "shopRoutes" object that holds the colde coming from "shop.js." - order matters! Before you call your "adminRoutes" code, you need to have the bodyParser object in place - now you implement your adminRoutes code that's going to have the appropriate code to use the bodyParser etc - same thing with your shopRoutes Here's your "adminRoutes.js" file...
const express = require("express"); const router = express.Router(); router.get("/add-product", (req, res, next) => { res.send( '<form action="/product" method="Post"><input type="text" name="title"><button type="submit">Add Product</button></form>' ); }); router.post("/product", (req, res, next) => { console.log(req.body); res.redirect("/"); }); module.exports = router;
import your express object import your Router object from Express notice how you're using "router.get" rather than "app.get" like you did previously because you're now having to incorporate the "router" dynamic same thing as #4 in that you're using "router.post" rather than "app.post" be sure to export your router object And here's your "shopRoutes.js" file...
const express = require("express"); const router = express.Router(); router.get("/", (req, res, next) => { res.send("<h1>Hello from Express</h1>"); }); module.exports = router;
Same sort of concepts that we just went over with the "adminRoutes.js" file! J) Adding a 404 Error Page (back to top...) Up to this point, we don't have any way to handle bogus page requests. For example, if the URL the user wanted to use was localhost:3000/asdf, we would get an error that made it look like we didn't know what we were doing as programmers. This will solve that: app.use((req, res, next) => { res.status(404).send("<h1>Page not found</h1>"); }); Remember, our code is going to be read from top to bottom. At this point, we've got two different scenarios being accommodated. One is the "/" dynamic and the other is "/add-product." Anything else and our page is going to explode. However, by using what we have above, which is taking advantage of Express' "status" method, we can funnel any rogue URL's through this option that will produce a legitimate page as opposed to a bogus error. K) Filtering Paths (back to top...) We can streamline the manner in which we might better qualify some of our routes by deploying a little shorthand. Here's the admin.js file:
const express = require("express"); const router = express.Router(); router.get("/admin/add-product", (req, res, next) => { // notice how we're now adding "admin" to the URL res.send( '<form action="/add-product" method="Post"><input type="text" name="title"><button type="submit">Add Product</button></form>' ); }); router.post("admin/add-product", (req, res, next) => { // and here as well console.log(req.body); res.redirect("/"); }); module.exports = router;
We can actually deploy a little shorthand to make it less complicated by adding "admin" to our "app.js" file like what you've got here: app.use('/admin', adminRoutes); By including "/admin," Express knows to be looking for that qualifier in a way that will not require us to specify that like you see above. Bear in mind, though, that this piece: router.get("/add-product", (req, res, next) => { res.send( '>form action="/admin/add-product" method="Post">>input type="text" name="title">>button type="submit">Add Product>/button>>/form>' ); }); ...you'll still need to include "admin/" to your "form action." L) Writing HTML (back to top...) So, we'll start by creating a "Views" directory, since our app is going to follow the MVC architecture format, and then we'll create an "add-product.html" page and a "shop.html" page. One thing that Express does which is kind of nice is pictured to the right. As soon as your "html," you get a pulldown and you can select the "html-5" option which will automatically create an HTML 5 template. Once we have that template in place, we'll write some basic HTML which looks like this:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Add Product</title> </head> <body> <header> <nav> <ul> <li><a href="/">Shop</a></li> <li><a href="/add-product">Add Product</a></li> </ul> </nav> </header> <main> <form action="/add-product" method="Post"> <input type="text" name="title"> <button type="submit">Add Product</button> </form> </main> </body> </html>
M) Rendering HTML (back to top...) To render some the HTML pages we're starting to write, here's what we do, as far as our routes are concerned:
const path = require("path"); const express = require("express"); const router = express.Router(); router.get("/", (req, res, next) => { res.sendFile(path.join(__dirname, "../", "views", "shop.html")); }); module.exports = router;
start by importing the "path" package from Express use res.sendFile(path.join) to get things moving and then use __dirname, "../", "shop.html")); to concatenate the path that shop.js is going to use to be able to reach the shop.html file. N) Returning a 404 Page (back to top...) Let's use the "routing" dynamic to return a 404 page. Pretty straight forward, actually... Here's your app.js file:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const app = express(); const adminRoutes = require("./routes/admin"); const shopRoutes = require("./routes/shop"); app.use(bodyParser.urlencoded()); app.use("/admin", adminRoutes); app.use(shopRoutes); app.use((req, res, next) => { res.status(404).sendFile(path.join(__dirname, "views", "404.html")); }); app.listen(3000);
- import the "path" module - use the "sendFile" dynamic along with the conjugation of what represents a path to the 404.html page O) Navigation Helper Function (back to top...)
Just as an aside, Node.js is a library in and of itself. Express is a framework that allows you to write code that constitutes a more streamlined approach to the syntax than what you would have to write otherwise. So, when you write const path = require('path');, you're calling a function from within the Node.js library. Express isn't even a part of the picture at that point...
This works: router.get("/", (req, res, next) => { res.sendFile(path.join(__dirname, "../", "views", "shop.html")); }); ...but the path dynamic is a little cumbersome. To streamline that, we'll use the "path" helper which is available through Express. It looks like this: First of all, set up a "routes.js" page in a directory we'll called "utility." On that page we'll write this:
const path = require("path"); module.exports = path.dirname(process.mainModule.filename);
1
bring in the "path" module
2
use the "dirname" function that's available to use within the "path" module as well as the whole path.dirname(process.mainModule.filename dynamic to settle on what it is that represents the home directory. Now, when I go to my "admin.js" file, I'll do this:
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const router = express.Router(); // /admin/add-product => GET router.get("/add-product", (req, res, next) => { res.sendFile(path.join(rootDir, "views", "add-product.html")); }); // /admin/add-product => POST router.post("/add-product", (req, res, next) => { console.log(req.body); res.redirect("/"); }); module.exports = router;
You call the "path" module, you then use the "rootDir" dynamic that is now being made available by the path.js page. You can now introduce some shorthand that makes the routing syntax a lot shorter and cleaner. P) Serving Files Statically (CSS) (back to top...) In order for Node to recognize the typical <link rel="stylesheet" href="/css/main.css"> code, you've got to bring in the app.use(express.static(path.join(__dirname, "public"))); dynamic. "static" refers to the way in which it's going to process links to that particular directory, which, in this case is, "public." Whereas before a typical reference to the "main.css" page wouldn't work, now it will. A) Sharing Data Across Requests and Users (back to top...) The idea is to take the data that the user is inputting and putting it into an array that we can then manipulate. Here's how we're going to do it:
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const router = express.Router(); const products = []; router.get("/add-product", (req, res, next) => { res.sendFile(path.join(rootDir, "views", "add-product.html")); }); router.post("/add-product", (req, res, next) => { products.push({ title: req.body.title }); res.redirect("/"); }); //module.exports = router; exports.routes = router; exports.products = products;
- set up a variable that's going to hold an array - push the incoming form data into the "products" array - we're changing up the way we're exporting things now so we can export some data as well as the routes - exporting the "products" array now... We're exporting that "products" array in a manner where we can now capture that puppy in the "app.js" file. Here's what that looks like...
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const adminData = require("./admin"); const router = express.Router(); router.get("/", (req, res, next) => { console.log(adminData.products); res.sendFile(path.join(rootDir, "views", "shop.html")); }); module.exports = router;
- set up a new const and grab what's being exported from the admin.js file - grab the "products" piece from what's being exported B) Template Engines (back to top...) The first thing you're going to do is install the Pug, EJS and the Handlebars template engines...
$ npm install --save ejs pug express-handlebars
We're going to start with Pug, so the next thing we're going to do is adjust our code so it's looking for a Pug template rather than a HTML page. Here we go... app.js Add this to your app.js file so your system knows to be looking for the Pug template engine. app.set("view engine", "pug"); app.set("views", "views"); "set" is used to establish a global dynamic. You'll notice with "const," we've got to call some "const" values every time we start a new page. With "set," you're "once and done!" Notice, the "view engine" dynamic. That means every "view" is going to be processed as something that's going to be processed via Pug. app.set is "views" by default. We're going it here for the sake of drill. That being the case, you now have to set up a Pug file in your Views directory. Bear in mind that every "view" is now looking for a Pug file. You don't have to change anything in your routing... 2) Pug Code and Dynamic Content (back to top...) Here's the original HTML compared to your new Pug template.
 HTML (shop.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Add Product</title> <link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/product.css"> </head> <body> <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a class="active" href="/">Shop</a></li> <li class="main-header__item"><a href="/admin/add-product">Add Product</a></li> </ul> </nav> </header> <main> <h1>My Products</h1> <p>List of all the products...</p> <!-- <div class="grid"> <article class="card product-item"> <header class="card__header"> <h1 class="product__title">Great Book</h1> </header> <div class="card__image"> <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book"> </div> <div class="card__content"> <h2 class="product__price">$19.99</h2> <p class="product__description">A very interesting book about so many even more interesting things!</p> </div> <div class="card__actions"> <button class="btn">Add to Cart</button> </div> </article> </div> --> </main> </body>
And here's the Pug template...
 Pug (shop.pug)
<!DOCTYPE html> html(lang="en") head meta(charset="UTF-8") meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(http-equiv="X-UA-Compatible", content="ie=edge") title #{docTitle} link(rel="stylesheet", href="/css/main.css") link(rel="stylesheet", href="/css/product.css") body header.main-header nav.main-header__nav ul.main-header__item-list li.main-header__item a.active(href="/" style="color:#fff;") Shop li.main-header__item a.active(href="/admin/add-product" style="color:#fff;") Add Product main if prods.length >0 .grid each product in prods article.card.product-item header.card__header h1.product__title #{product.title} div.card__image img(src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png", alt="A Book") div.card__content h2.product__price $19.99 p.product__description A very interesting book about so many even more interesting things! .card__actions button.btn Add to Cart else h1 No Products
You can kick off your Pug template the same way you did with your HTML docs in that you type "html" and you'll get a pulldown of possible HTML doctypes. Choose "5" and Express / Pug will give you a Pug HTML header. Notice that you're not using any delimiters with Pug. Also, indentation is key. You MUST indent in order for the code to be rendered / read accurately. Notice you're "title" tag. In your static HTML file, you've got this: <title>Add Product</title> In your Pug file, you've got this: title #{docTitle} This is coming from your admin.js / shop.js file. Remember? In your admin.js file, you've got this:
1
const products = []; router.get("/add-product", (req, res, next) => { res.sendFile(path.join(rootDir, "views", "add-product.html")); }); router.post("/add-product", (req, res, next) => {
2
products.push({ title: req.body.title }); res.redirect("/"); }); //module.exports = router; exports.routes = router;
3
exports.products = products;
What's a const? It's an empty digital container until you put something in it. In this case, it's an empty array we're calling "products."
1
You establish your "products" const.
2
When we post something, we're "pushing" something into the products array and that "something" is whatever the value associated with the "title" input field may be... <input type="text" name="title" id="title"> - this is coming from the "add-product.html" page So, we push someting into that array and then we're exporting it at the bottom of the page by using this:
3
exports.products = products; If there's something in that array, it will be available to us in the context of the "products" value it's exporting. Now, let's take a look at "shop.js..."
1
const adminData = require("./admin"); const router = express.Router(); router.get("/", (req, res, next) => {
2
const products = adminData.products;
3
res.render("shop",
4
{ prods: products, docTitle: "Shop" });
5
//res.sendFile(path.join(rootDir, "views", "shop.html")); });
1
we grab what's being exported from the "admin.js" file in the "routes" directory. That's going to include the "products" piece (exports.products = products;)
2
now we grab the "products" from what's being exported by the "admin.js" page
3
this is different! Notice the code that we've got commented out. This code is what's going to allow Pug to see the dynamic content we're sending over with "res.render." The first argument is the name of the Pug file that we're targeting which, in this case, is "shop."
4
we're using curly braces to specify the variables we're sending over to the "shop.pug" file. In this case, we've got a variable called "prods" which has our "products" array and we're also including another variable called "docTitle." This is !
5
This is the original code we were using when we were simply calling an HTML file All of this is now properly packaged for the Pug file! The first thing that we did is grab the "docTitle" variable that was coming to us from "shop.js." That was .
Next, you've got "main." Notice, no delimters and, again, indentation is huge. With , you've got a loop that's going to enumerate everything in the "prods" variable which was
4
from "shop.js." In this case, the only key value pair is the "title -> book name" which, again, is coming from "shop.js" which is being generated from what's being exported from "admin.js." And finally, is your closing "if" statement in that if your array is empty, you'll get the "No Products" text. Let's take a look now at the "Add Product" page as a Pug doc...
<!DOCTYPE html> html(lang="en") head meta(charset="UTF-8") meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(http-equiv="X-UA-Compatible", content="ie=edge") title #{pageTitle} link(rel="stylesheet", href="/css/main.css") link(rel="stylesheet", href="/css/forms.css") link(rel="stylesheet", href="/css/product.css") body header.main-header nav.main-header__nav ul.main-header__item-list li.main-header__item a(href="/" style="color:#fff;") Shop li.main-header__item a.active(href="/admin/add-product" style="color:#fff;") Add Product main form.product-form(action="/admin/add-product", method="POST") .form-control label(for="title") Title input(type="text", name="title")#title button.btn(type="submit") Add Product
This is going to be rendered by the "admin.js" file like this... router.get("/add-product", (req, res, next) => { res.render("add-product", { pageTitle: "Add Product" }); }); And finally, you've got the "404.html" page. Here's how that looks as a Pug doc...
<!DOCTYPE html> html(lang="en") head meta(charset="UTF-8") meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(http-equiv="X-UA-Compatible", content="ie=edge") title Page Not Found link(rel="stylesheet", href="/css/main.css") body header.main-header nav.main-header__nav ul.main-header__item-list li.main-header__item a(href="/" style="color:#fff;") Shop li.main-header__item a.active(href="/admin/add-product" style="color:#fff;") Add Product h1 Page Not Found
And that's going to be rendered in your "app.js" file and that's going to look like this: app.use((req, res, next) => { //res.status(404).sendFile(path.join(__dirname, "views", "404.html")); res.status(404).render("404"); }); 2) Using Layouts / Extends (back to top...) This last piece shows how to use "extends" which gives you the opportunity to do something to like PHP's require when it comes to repetitive content. First, let's look at the "app.js" page: i) app.js (back to top...)
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const app = express(); app.set("view engine", "pug"); app.set("views", "views"); const adminData = require("./routes/admin"); const shopRoutes = require("./routes/shop"); app.use(bodyParser.urlencoded()); app.use(express.static(path.join(__dirname, "public"))); app.use("/admin", adminData.routes); app.use(shopRoutes); app.use((req, res, next) => { //res.status(404).sendFile(path.join(__dirname, "views", "404.html")); res.status(404).render("404", { pageTitle: "Page Not Found" }); }); app.listen(3000);
you're passing the "pageTitle" variable into the "main-layout.png" file. ii) main-layout.pug (back to top...) In "views->layouts," you now have file called "main-layout.pug." Here's the way that looks:
<!DOCTYPE html> html(lang="en") head meta(charset="UTF-8") meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(http-equiv="X-UA-Compatible", content="ie=edge") title #{pageTitle} link(rel="stylesheet", href="/css/main.css") block styles body header.main-header nav.main-header__nav ul.main-header__item-list li.main-header__item a(href="/" class=(path==="/" ? 'active' : '') style="color:#fff;") Shop li.main-header__item a(href="/admin/add-product", class=(path==="/admin/add-product" ? 'active' : '') style="color:#fff;") Add Product block content
"block" represents a piece of code that Pug recognizes as a placeholder for forthcoming content. In this case, it's going to be your "styles" content some "if" shorthand that's evaluating the incoming "path" variable to determine whether or not the class needs to be "active" some more "if" shorthand that's evaluating the incoming "path" variable to determine whether or not the class needs to be "active" another "block." This is for your content. iii) admin.js (back to top...) For "admin.js," the only code you're altering here is the "pageTitle" variable that the "main-layout.pug" file is going to be looking for. router.get("/add-product", (req, res, next) => { res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product" }); }); ...same sort of thing for "shop.js..." iv) shop.js (back to top...) router.get("/", (req, res, next) => { const products = adminData.products; res.render("shop", { prods: products, pageTitle: "Shop", path: "/" }); //res.sendFile(path.join(rootDir, "views", "shop.html")); }); v) add-product.pug (back to top...) Here is where things start looking sharp! This is your "shop.pug" file:
extends layouts/main-layout.pug block styles link(rel="stylesheet", href="/css/forms.css") link(rel="stylesheet", href="/css/product.css") block content main if prods.length >0 .grid each product in prods article.card.product-item header.card__header h1.product__title #{product.title} div.card__image img(src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png", alt="A Book") div.card__content h2.product__price $19.99 p.product__description A very interesting book about so many even more interesting things! .card__actions button.btn Add to Cart else h1 No Products
this first "block" is your styles - this is your content And here's your "Add-Product" page...
extends layouts/main-layout.pug block styles link(rel="stylesheet", href="/css/forms.css") link(rel="stylesheet", href="/css/product.css") block content main form.product-form(action="/admin/add-product", method="POST") .form-control label(for="title") Title input(type="text", name="title")#title button.btn(type="submit") Add Product
Same kind of dynamic. Much cleaner and much easier! You get the idea! 3) Handlebars (back to top...) Handlebars is another template engine that uses a little more HTML and less markup. Here's how get that motor running in your "app.js" file:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const expressHbs = require("express-handlebars"); // you've got to first import it into your app const app = express(); app.engine("hbs", expressHbs()); app.set("view engine", "hbs"); app.set("views", "views"); const adminData = require("./routes/admin"); const shopRoutes = require("./routes/shop"); app.use(bodyParser.urlencoded()); app.use(express.static(path.join(__dirname, "public"))); app.use("/admin", adminData.routes); app.use(shopRoutes); app.use((req, res, next) => { //res.status(404).sendFile(path.join(__dirname, "views", "404.html")); res.status(404).render("404", { pageTitle: "Page Not Found" }); }); app.listen(3000);
Just for the sake of review / clarification: An object in computer language is something you can actually use and measure. In PHP, when you instantiate a Class, you are creating an "instance" of that class. You call that "instance" an object. Up until that point, while the Class might exist, it's like a greyed-out button. It has the capacity to perform and function, but until you create an instance of it and make it into something you can "click on" and utilize all of the functionality it represents - until it's an "object" - it just sits there. In this case, when you import Express into your app, you do so by assigning it to a constant. At that point, you've got a greyed-out button. It's when you make it "clickable" by assigning what you've imported to the "app" object, now you've got something you can use! And, once it's useable, you can tap anyone of number of different methods contained within that collection of functionalities.
while Pug is automatically installed with Express, Handlebars is not and that's why you have to import it even after you install it now that the Handlebars engine has been imported, you still have to let Express know that it exists. You do that using the "engine" method that's a part of the "app" object. You're doing this a little bit differently than you did before. With Pug, you did this: app.set("view engine", "pug"); You did it that way because Pug comes with Express. With Handlebars, you've got specify it by using the "engine" method within the "app" (Express) object. The sytnax that you're using takes "expressHbs()," which Express understands to be the Handlebars library, and associates it with the first variable you specify in the line of code which, in this case is "hbs." That's going to be the extension of your "Handlebar" docs. Once you've got access to the Handlebars paradigm and you've got pointed it to the characters you're going to use to identify your "Handlebars" doc type, you set your global view dynamic to be such that it's looking for "hbs" file types. 4) Convert Project to Handlebars (back to top...) To convert our project to a Handlebars paradigm, we're going to do make the following changes... i) add-products.hbs (back to top...) To the "Add-Products" page, you're just going to uncomment what you had in terms of pure HTML and let that be your "add-product.hbs" page. Like this:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{{ pageTitle }}</title> <link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/forms.css"> <link rel="stylesheet" href="/css/product.css"> </head> <body> <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a href="/">Shop</a></li> <li class="main-header__item"><a class="active" href="/admin/add-product">Add Product</a></li> </ul> </nav> </header> <main> <form class="product-form" action="/admin/add-product" method="POST"> <div class="form-control"> <label for="title">Title</label> <input type="text" name="title" id="title"> </div> <button class="btn" type="submit">Add Product</button> </form> </main> </body> </html>
That part is easy. For "shop.hbs," you've a couple of things that need to change, both with that file and with "shop.js." ii) shop.hbs (back to top...) For "shop.js," you've got this:
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const adminData = require("./admin"); const router = express.Router(); router.get("/", (req, res, next) => { const products = adminData.products; res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0 }); //res.sendFile(path.join(rootDir, "views", "shop.html")); }); module.exports = router;
not a big deal, but something to keep in mind for Handlebars. Handlebars doesn't look for the "length" of a particular value. Rather, it's going to be looking for boolean values - either "true" or "false." hasProducts: products.length >0 is an "if" statement that assigns the "hadProducts" variable either a "true" or "false" based on whether or not the lengh of your products array is greater than 0. That's important because of what comes next: This is your "shop.hbs" page...
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{{ pageTitle }} </title> <link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/product.css"> </head> <body> <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a class="active" href="/">Shop</a></li> <li class="main-header__item"><a href="/admin/add-product">Add Product</a></li> </ul> </nav> </header> <main> {{#if hasProducts }} <div class="grid"> {{#each prods}} <article class="card product-item"> <header class="card__header"> <h1 class="product__title">{{ this.title }}</h1> </header> <div class="card__image"> <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book"> </div> <div class="card__content"> <h2 class="product__price">$19.99</h2> <p class="product__description">A very interesting book about so many even more interesting things!</p> </div> <div class="card__actions"> <button class="btn">Add to Cart</button> </div> </article> {{/each}} </div> {{else}} <h1>No Products Found</h1> {{/if}} </div> </main> </body> </html>
grabbing your page title dynamically here's the boolean value we set up in "shop.js" to determine whether or not you have to iterate through an array that may or may not exist here's how you loop through an array. In this case, we're iterating through "prods." here's how you close your "each" loop here's how you introduce an "else" dynamic here's how your close your "if" clause 5) Using Layouts in Handlebars (back to top...) Using Layouts in Handlebars in similiar to Pug, but with some minor tweaks. i) app.js (back to top...) First off, go to "app.js" and let the system know where to be looking for your layout files. You do that by adding this to your "app.engine" code: app.engine( "hbs", expressHbs({ layoutsDir: "views/layouts/", // where you're keeping your "layout" file defaultLayout: "main-layout", // the name of your default Layout file extname: "hbs" // something that is unique to Handlebars in that you have to define the extension name of the file you're using for your layout page }) ); Before, all you had was this: app.engine("hbs", expressHbs()); ii) main-layout.hbs (back to top...) Now, you're adding your "layouts" dynamic. We'll use the "404.html" page as our scaffolding...
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{{ pageTitle }}</title> <link rel="stylesheet" href="/css/main.css"> {{#if formsCSS}} <link rel="stylesheet" href="/css/forms.css"> {{/if}} {{#if productCSS}} <link rel="stylesheet" href="/css/product.css"> {{/if}} </head> <body> <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a class="{{#if activeShop }}active{{/if}}" href="/">Shop</a></li> <li class="main-header__item"><a class="{{#if activeAddProduct }}active{{/if}}" href="/admin/add-product">Add Product</a></li> </ul> </nav> </header> {{{ body }}} </body> </html>
This is dynamic content You're passing your "pageTitle" in from your "shop.js," "admin.js" and your "app.js" files. Here you're doing something a little different because of the way "Handlebars" is wired. With Pug, you could do "block styles." With Handlebars, you need to throw some "if" clauses into the mix in order to accommodate the different styling you're going to want depending on the page you're on. An inline "if" clause that's grabbing the "active" variable and gauging it to determine whether or not the link in question needs to have the class of "active." Now, let's set up our other routes to declare the variables our "layout" is going to be looking for and then we'll craft our pages knowing that the new "layout" dynamic is in place. iii) admin.js (back to top...) The things that we're sensitive to here are in yellow:
router.get("/add-product", (req, res, next) => { res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product", formsCSS: true, productCSS: true, activeAddProduct: true }); });
...and you'll see those variables being plugged in as you see below. <title>{{ pageTitle }}</title> <link rel="stylesheet" href="/css/main.css"> {{#if formsCSS}} <link rel="stylesheet" href="/css/forms.css"> {{/if}} {{#if productCSS}} <link rel="stylesheet" href="/css/product.css"> {{/if}} and here... <li class="main-header__item"><a class="{{#if activeShop }}active{{/if}}" href="/">Shop</a></li> <li class="main-header__item"><a class="{{#if activeAddProduct }}active{{/if}}" href="/admin/add-product">Add Product</a></li> iv) shop.js (back to top...) To use the "layout" dynamic on the "shop.js" page, you'll do this:
router.get("/", (req, res, next) => { const products = adminData.products; res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true });
Now that you've got your routes squared away, specifically in terms of fowarding the expected variables to your "new and improved" hbs files that are now using the "main-layout.hbs" file, this is what you've got where your "add-product.hbs" page is concerned: v) add-product.hbs (back to top...)
<main> <form class="product-form" action="/admin/add-product" method="POST"> <div class="form-control"> <label for="title">Title</label> <input type="text" name="title" id="title"> </div> <button class="btn" type="submit">Add Product</button> </form> </main>
Pretty intuitive. Everything is now being handled by your "main-layout.hbs" file, as far as header information etc. You'll notice that you don't have to "call" that file like you had to do with Pug. And that's it! 6) EJS(back to top...) EJS stands for "Embedded JavaScript Templates," and is the template engine of choice in this tutorial. First thing, we're going to our "app.js" file and change our "view engine:" app.set("view engine", "ejs"); Notice this is a lot less cumbersome than "Handlebars" where you had to import the library, instantiate it, etc. Now that you've done that, Express knows to be looking for an "ejs" file for every page that needs to be rendered. One thing to be aware of is that we don't use "layouts" with EJS, but you still have access to an "include" dynamic and we'll get to that in a minute. Let's start with the "404" page... i) 404.ejs (back to top...) You'll notice that you can write a lot of pure JavaScript and HTML code with this paradigm...
<%- include('includes/head.ejs') %> </head> <body> <%-include('includes/navigation.ejs') %> <h1>Page Not Found!</h1> <%-include('includes/end.ejs') %>
You'll notice the use of "include." This is EJS's way of referencing files that constitute common denominators when comparing one page to another. Here, as in the other pages, you see the "head.ejs," the "navigation.ejs" and the "end.ejs." Here are those files: - head.ejs (back to top...) The EJS code that you'll use in this situation is <%-. In a little bit we're going to be using <%=. The difference is that with <%= we're going for a value. With <%- we're letting the system know that it's getting ready to render some HTML rather than strict text. That said, it's obvious we're getting ready to render some HTML and here's how that's going to look like in our "head.ejs" template...
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>
1
<%= pageTitle %></title> <link rel="stylesheet" href="/css/main.css" /> </head> </html>
1
grabbing the page title dynamically... - navigation.ejs (back to top...) Same kind of thing with our navigation. We're not printing text, we're rendering code as per the <%- dynamic. Here's that that "navigation.ejs" code:
<header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a class="<%= path === '/' ? 'active' : ''%>" href="/">Shop</a></li> <li class="main-header__item"> <li class="main-header__item">
1
<a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product">Add Product</a></li> </li> </ul> </nav> </header>
1
Notice the "if" statement that controls whether or not a particular link is "active." And that's going to be coming from the corresponding "js" file. So, for example, if you've got the "shop" page being rendered, the "shop.js" file has this: router.get("/", (req, res, next) => { const products = adminData.products; res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); We've gone through this before, but you know... - end.js (back to top...) </body> </html> ii) add-product.ejs (back to top...)
<%- include('includes/head.ejs') %> <link rel="stylesheet" href="/css/forms.css" /> <link rel="stylesheet" href="/css/product.css" /> </head> <body> <%- include('includes/navigation.ejs') %> <main> <form class="product-form" action="/admin/add-product" method="POST"> <div class="form-control"> <label for="title">Title</label> <input type="text" name="title" id="title" /> </div> <button class="btn" type="submit">Add Product</button> </form> </main> <%- include('includes/end.ejs') %>
Notice how you're grabbing the "common" files in your header, but you are grabbing the CSS files that are unique to this particular page. iii) shop.ejs (back to top...)
<%- include('includes/head.ejs') %> <link rel="stylesheet" href="/css/product.css" /> </head> <body> <%- include('includes/navigation.ejs') %> <main> <h1>My Products</h1> <p>List of all the products...</p> <% if(prods.length>0) { %> <div class="grid"> <% for (let product of prods) { %> <article class="card product-item"> <header class="card__header"> <h1 class="product__title"><%= product.title %></h1> </header> <div class="card__image"> <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book" /> </div> <div class="card__content"> <h2 class="product__price">$19.99</h2> <p class="product__description"> A very interesting book about so many even more interesting things! </p> </div> <div class="card__actions"> <button class="btn">Add to Cart</button> </div> </article> <% } %> </div> <% } else { %> <h1>No Products Found</h1> <% } %> </main> <%- include('includes/end.ejs') %>
looking to see if we've got anything in the "products" array which is coming from "admin.js..."
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const router = express.Router(); const products = []; router.get("/add-product", (req, res, next) => { res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product", formsCSS: true, productCSS: true, activeAddProduct: true }); }); router.post("/add-product", (req, res, next) => { products.push({ title: req.body.title }); res.redirect("/"); }); //module.exports = router; exports.routes = router; exports.products = products;
If there's any content in the array, the code proceeds to iterate through the array Here's how you close the loop Here's how you close the "if" clause And that's it, baby! Here's another assignment and a great spot to do a review of how to set up an app and what code needs to be where in order to accomplish what we've looked at thus far... A) Setup (back to top...) To set up a new app, you're going to: set up the directory npm init - that initializes your project with Node and puts your "package.json" file in place install nodemon - you do that by entering npm install nodemon --save -dev Setup Shortcuts -pop the hood on your "package.json" file and set up your "start" shortcut with "start": "nodemon app.js" Install Express - you install express by typing npm install --save express i) Starting Point of a Working App (back to top...) Remember, when you install "Express," you're geting a lot of functionality in place by default. Your "bare bones" app.js file will look like this: const express = require("express"); const app = express(); app.listen(3000); console.log("good to go"); You don't have to declare your "http" object or your "server." Remember that because otherwise you'll get some errors! At this point, you've got a working page / app! Having said that, let's take a look at what our "app.js" file is going to look like and break it down... B) app.js (back to top...)
6
const path = require('path');
7
const express = require('express');
8
const bodyParser = require('body-parser');
9
const app = express();
10
app.set('view engine', 'ejs'); app.set('views', 'views');
11
const adminData = require('./routes/admin'); const displayData = require('./routes/display');
12
app.use(bodyParser.urlencoded());
13
app.use(express.static(path.join(__dirname, 'public')));
14
app.use("/admin", adminData.routes); app.use(shopRoutes);
15
app.use((req, res, next) => { //res.status(404).sendFile(path.join(__dirname, "views", "404.html")); res.status(404).render("404", { pageTitle: "Page Not Found" }); }); app.listen(3000);
require("path")
- in order for your app to understand and process routes, you've got to import the "path" module from Node. You'll put that piece of syntax at the top of your code. That line looks like this: const path = require("path");
const express=require('express');
After you set up your "path" dynamic, import Express.
const bodyParser = require('body-parser');
"body-parser" is processing your incoming data and making it readable to your app.
const app = express();
You've imported Express, now you're going to register it and be able to access all of its functionality by packaging it in the "app" constant.
view engine
- here is where you define your Templating Engine. In this case, we're using "EJS." You use "set as part of Express' way to establish a dynamic that affects the server and not just the app. You can think of it as a "global" setting. app.set("view engine", "ejs"); app.set("views", "views");
create routes
Notice how up to this point you've not written one snippet of HTML code. That's getting ready to change. But before we do that, we need to define our routes. Your "routes" are made possible by the "path" helper we imported earlier. That's what allows Node to make sense of any kind of reference facilitated by a URL. - set up a new directory called "routes." You'll have two new files in there. One will be called "admin.js" and the other will be called "display.js." On your "app.js" page, you'll write this: const adminData = require('./routes/admin'); const displayData = require('./routes/display'); What you're doing at this point is you're establishing on your app.js page - the page that's going to be accessed by your app by default - the content that's coming from these two "routes."
IMPORTANT! You're importing these pieces of code. In other words, you're making sure they're on the shelves of your library but you haven't actually checked them out yet. That's coming in just a moment.
app.use(bodyParser.urlencoded());
app.use(bodyParser.urlencoded()); - with this, you're registering the "body-parser" you imported earlier.
BTW: app.use is what you're coding in order to utilize a piece of Express that you've imported earlier. You can access a really good description of the "app.use" method by clicking here. What's especially relevant about this little piece of syntax is the way in which you can access the "request and "response" objects!
You're going to see this from here on out. "app" is what's holding your "Express" paradigm. Anytime you use "app.use," you're identifying either something you've imported or something that's a part of the Express dynamic by default and "activating" that functionality.
express.static(path.join(__dirname, 'public'))
app.use(express.static(path.join(__dirname, 'public'))) is what you're using for Node to be able to recognize static files, like <link rel="stylesheet" href="/css/main.css"> for example. Click here for more information.
app.use("/admin", adminData.routes);
app.use("/admin", adminData.routes); - here is where you're registering the route dynamic you imported a few lines ago. In this instance, you're "activating" the "adminData" dynamic.
404 page
Like what has been described before, up to this point, as your app is being read top to bottom, the only routes that are being defined as legitimate is the "admin" and the "display" route. Anything else, and your app doesn't know what to do. This... app.use((req, res, next) => { //res.status(404).sendFile(path.join(__dirname, "views", "404.html")); res.status(404).render("404", { pageTitle: "Page Not Found" }); }); ...fixes that. Now, your app will know to advance your user to the "404" page if something goes south and there's no route that matches what the user has selected. Click here for a refresher.
C) routes/admin.js (back to top...) The "routes" dynamic is what controls your content. Much like the "model" aspect of the MVC architecture, you're going to use this part of your app to dictate what is being displayed on your page. It's not just a URL. This has got some content and flow to it. Here we go...
1
const path = require('path');
2
const express = require('express');
3
const rootDir = require('../utility/path');
4
const router = express.Router();
5
const products = [];
6
router.get("/add-product", (req, res, next) => res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product", formsCSS: true, productsCSS: true, activeAddProduct: true }); });
7
router.post("/add-product", (req, res, next) => { products.push({ title: req.body.title }); res.redirect("/"); });
8
exports.routes=router;
9
exports.products=products;
const path = require('path');
- the first thing you're going to do is import the "path" module from Node. This is what allows your app to understand the various paths to routes, pages and functionality
const express = require('express');
- import Express. Remember, this is what allows you to write a lot of code with minimal sytax. That's the whole point of Express.
const rootDir = require('../utility/path');
- you've imported the "path" object from Node. Now you can use it to reference your "path.js" file in the "utility" directory. This is just a shorthand script to reference what represents your "home" directory. We'll write that piece of code / page in just a little bit.
const router = express.Router
- the Router object is the Express piece that allows you to interpret and use form data (POST, GET etc) as well as your page's data in general. For more info about that, click here.
const products = []
- empty products array...
6
here you're setting up the page content when you first access it. Here's how the code looks:
router.get("/add-product", (req, res, next) => { res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product", formsCSS: true, productCSS: true, activeAddProduct: true }); }); Just as a reminder: res.render: this is for what's going to be your EJS file. This renders a view and sends it to the EJS client pageTitle: this is a variable you're sending to your EJS client. It's the dynamic content that we'll have on all our pages. path: this is the actual path the user will access to get to this page formsCSS, productCSS and activeAddProduct are CSS files and the variable that governs the "active" condition of the page that is active
7
here's how you're "posting" your data into the array that will be seen on your display page
router.post("/add-product", (req, res, next) => { products.push({title:req.body.title}); res.redirect("/"); }); Again, as a reminder: router.post: - this is your "post" command that's handling your form data. products.push: - we're "pushing" the form content into the "products" array res.redirect: - we're redirecting the user after we're done processing the form back to the index page
8
export.routes = router;
Where "path" is about URL, "router" is more about content. That's a good way to envision it in order to prevent confusion when you're looking at the term "route." In this case, we're looking at what is being "posted" by the form and how that is to be handled. Here, it's going to be maintained in an array called, "products."
9
export.products = products;
This is the other piece that's being "exported" from this page and being made available to the system in general. In this case, it's the "products" array.
D) routes/display.js (back to top...) Again, this is the "model" aspect of the app and this one is disseminating content to the "display" view that you're going to be creating in a minute. Let's take it apart:
1
const path = require("path");
2
const express = require("express");
3
const rootDir = require("../utility/path");
4
const adminData = require('./admin');
5
const router = express.Router();
6
router.get("/", (req, res, next) => { const products = adminData.products = adminData.products; res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); });
7
module.exports = router
const path = require('path');
- the first thing you're going to do is import the "path" module from Node. This is what allows your app to understand the various paths to routes, pages and functionality
const express = require('express');
- import Express. Remember, this is what allows you to write a lot of code with minimal sytax. That's the whole point of Express.
const rootDir = require('../utility/path');
- you've imported the "path" object from Node. Now you can use it to reference your "path.js" file in the "utility" directory. This is just a shorthand script to reference what represents your "home" directory. We'll write that piece of code / page in just a little bit.
const adminData = require('./admin');
There are two things being exported from the "admin.js" file in your "routes" directory and that's your "POST-ed" data as well as the data in your "products" array. With this piece of code, we're getting access to everything and we'll be more specific with what we want in a minute...
const router = express.Router
- the Router object is the Express piece that allows you to interpret and use form data (POST, GET etc). For more info about that, click here.
here is where you're "getting" your content and then exporting at the end of your page
router.get("/", (req, res, next) => { const products = adminData.products = adminData.products; res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); });
In this tutorial, you started off by writing straight HTML. In order to render that via Node, you had to use res.sendFile. That's what Node uses to "deliver" any kind of file. The syntax used to facilitate all of that is: router.get("/", (req, res, next) => { res.sendFile(path.join(__dirname, "../", "views", "shop.html")); }); When you use EJS, you're not going to need the "res.sendFile" dynamic at all. Instead, you'll used "res.render."
Most of the above elements have been already discussed, but there's one piece that's worth a second look and that's the "hasProducts" variable. That is an "IF" statement that looks for the length of the "products" array. If there's some content, then the "activeShop" variable is set to true as well as the "productCSS" variable.
here is where you're "getting" your content and then exporting at the end of your page
BTW: "module" is something that's a part of the Node.js library. It's a variable that refers to the current "module." When you use it in conjuction with another object like, in this instance, "exports," you're saying, "Whatever variable called "exports" that exists in the context of this application module, bring that to the table!" You can read more about that syntax by clicking here
module.exports=router; - this is exporting everything that's been assigned to your "router" variable above.
E) utility/path.js (back to top...) On your two "routes" pages (admin and display), we introduced a little shortcut for the "root" directory, remember? It looks like this: const rootDir = require("../utility/path"); To review this concept, click here. Bascially, what you're doing is using the "path" module that's a part of Node and then using the "dirname" function to represent what represents the "home" directory for your app. So, after you've created your "utility" directory, you'll write this for your "path.js" page: const path = require("path"); module.exports = path.dirname (process.mainModule.filename); F) header.ejs (back to top...) Now we're getting into our "Views." The first thing we're going to build is something that every page is going to share and that is the, "header." First, set up your "views" directory and then in that folder create an "includes" directory. In that directory, we'll put our "header.ejs" file. Remember, you can set up a "stunt" html file and use a function that "Express" offers, as far as being able to type "html" and "Express" will provide all the header code you need for an HTML 5 page. Click here for a refresher. Some of your content is going to be dynamic, so let's take a look at what your code is going to be: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title><%= pageTitle %></title> dynamic content that will provide our "pageTitle" which we'll pass on as a variable <link rel="stylesheet" href="/css/main.css" /> </head> </html> G) footer.ejs (back to top...) Nothing especially noteworthy here. This is just the "caboose" for every page: </body> </html> H) navigation.ejs (back to top...) This is your third and last "include." This is the "navigation" bar. The one thing you want to be sensitive to here is the way in which the code dynamically asserts the "active" dynamic for the links...
<header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item">
1
<a class="<%= path === '/' ? 'active' : ''%>" href="/">Shop</a></li> <li class="main-header__item"> <li class="main-header__item">
2
<a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product">Add Product</a></li> </li> </ul> </nav> </header>
Navigation "IF Statement for "Index" Page
What's happening here, and it's actually pretty clever, you're looking at path and using that to dictate whether or not the link is going to be stylized as an "active" link. <a class="<%= path === '/' ? 'active' : ''%>" href="/">Shop</a>
Navigation "IF Statement for "Admin" Page
Same thing as above in that you're looking at the path and using that to dictate whether or not the link is going to be stylized as an "active" link. <a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product">Add Product</a>
I) public / css (back to top...) Not a whole lot to figure out here. You're just setting up a "public" folder in your main directory and putting your three CSS files in there. J) add-name.ejs (back to top...)
<%- include('includes/header.ejs') %> <link rel="stylesheet" href="/css/forms.css" /> <link rel="stylesheet" href="/css/product.css" /> </head> <body> <%- include('includes/navigation.ejs') %> <main> <form class="product-form" action="/admin/add-name" method="POST"> <div class="form-control"> <label for="title">Title</label> <input type="text" name="title" id="title" /> </div> <button class="btn" type="submit">Add Name</button> </form> </main> <%- include('includes/footer.ejs') %>
Most of the above syntax has already been explained. Your "pageTitle" is dynamic content that's going to be coming from your "route" file, and we'll see that in just a minute. K) admin.js (back to top...) admin.js is the code that's publishing your names to the "display.js" page. Here's how it breaks down:
1
const path = require("path");
2
const express = require("express");
3
const rootDir = require("../utility/path");
4
const router = express.Router();
5
const products = [];
6
router.get("/add-name", (req, res, next) => { res.render("add-name", { pageTitle: "Add Name", path: "/admin/add-name", formsCSS: true, productsCSS: true, activeAddProduct: true }); });
7
router.post("/add-name", (req, res, next) => { products.push({ title: req.body.title }); res.redirect("/"); });
8
exports.routes = router;
9
exports.products = products;
const path = require("path");
Again, this is review. But it's good review! Click here to see the section where this was initially discussed. The first thing you're going to do is grab your "path" module from Node and assign it to the "path" constant. This is ensuring that your app understands the various routes that are going to be ascertained from the various URL's that comprise your application.
const express = require("express");
Now, you're importing Express.
const rootDir = require("../utility/path");
Establishing your root directory so you can establish the directory from which all URLs are going to be relative to. Specifically, it's shorthand so you can reference your "utility" directory.
const router= express.Router();
Here's the module that allows you to process form data as well as disseminate data to your various pages in general.
const products=[];
Setting up your empty array.
router.get...
This piece of the code is what's populating your "add-name" page - the form that allows users to add names to the "products" array.
router.post...
This section is "posting" the "title" variable to your "products" array and keeps adding to it in the context of the "push" dynamic.
exports.routes=router;
As was mentioned previously, "path" is about URL, "router" is about content. This particular "router" is about your page's content and is reaching for the "add-name" view in your "views" directory.
exports.products=products;
This is your "products" array. And that's a working app! BTW: On this assignment, I had to install EJS and "body-parser" separately...
Up to now, we haven't taken the time to organize things according to what is referred to as a "separation of concerns." This is where you're separating your database functionality from your display functionality etc. A) The Controller (back to top...) By definition, the Controller is serving as the intermediary logic between the data and the view. Given that definition, you can see how we've been doing that very thing in the context of our routes. That's where we'll be grabbing some of the code and putting that into our new "controllers" directory. Also, while we've got a "shop.js" file and an "admin.js" file, the files overlap with one another in the way they interact with the "products" table. That's why you'll see "shop" and "admin" functionality on the one, new "products.js" file in the "controller" directory. Here we go... 1) admin.js (back to top...) This is the "admin.js" file as it currently exists: admin.js is the code that's publishing your names to the "display.js" page. Here's how it breaks down:
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const router = express.Router(); const products = [];
1
router.get("/add-name", (req, res, next) => { res.render("add-name", { pageTitle: "Add Name", path: "/admin/add-name", formsCSS: true, productsCSS: true, activeAddProduct: true }); });
2
router.post("/add-name", (req, res, next) => { products.push({ title: req.body.title }); res.redirect("/"); });
3
exports.routes = router;
4
exports.products = products;
Pause for a moment and reflect on the "content" of what you're looking at. Basically, you've got only two different scenarios to contend with: Your "get" and your "post."
1
router.get...
Your user has just accessed the "add-name" URL. This code is defining the content of that page. We're going to move that to our "products" controller by writing this: First, we'll define our "productsController" by writing this at the top of the page: const productsController = requre('../controller/products'); Then we'll write our "new and improved" code by writing this. Notice how we're now invoking the "productsController" page now. router.get('/add-product', productsController.getAddProduct);
2
router.post...
And this mess is going to be replaced with this: router.post("/add-product", productsController.postAddProduct);
3
exports.routes = router
Theoretically, you've now "packed" both your "post" and your "get" functionality into one "route" variable and that's what you're exporting.
4
exports.products = products
The "products" you had originally is relevant now only in the context of the "productsController" page, which we'll look at in a minute. Everything except the numbered lines are getting ready to change. Here's how it looks now (sans the "exports.products = products" line):
const path = require("path"); const express = require("express"); const productsController = requre('../controller/products'); // here's your new ProductsController const router = express.Router();
1
router.get('/add-product', productsController.getAddProduct);
2
router.post("/add-product", productsController.postAddProduct);
3
module.exports = router;
1
router.get('/add-product', productsController.getAddProduct); Now, you're referencing the previously documented syntax in the context of your "productsController." The code itself is pretty intuitive. You've got the ('/route', nameofController.methodWithinController); The same goes for your "add-product" syntax and then you've got the "router," holding all your syntaxial excellence being exported.
2
router.get('/add-product', productsController.postAddProduct); Just like we said in the above line - you're now referencing that code rather than explicity documenting here.
3
module.exports = router; Whereas before you exporting the "products" dynamic, now you're just sending out what you've got represented by the "router." Here's how everything looks when it' side by side:
old admin.js
  1. const path = require("path");
  2. const express = require("express");
  3. const rootDir = require("../utility/path");
  4. const router = express.Router();
  5. const products = [];
  6. router.get("/add-name", (req, res, next) => {
  7. res.render("add-name", {
  8. pageTitle: "Add Name",
  9. path: "/admin/add-name",
  10. formsCSS: true,
  11. productsCSS: true,
  12. activeAddProduct: true
  13. });
  14. });
  15. router.post("/add-name", (req, res, next) => {
  16. products.push({ title: req.body.title });
  17. res.redirect("/");
  18. });
  19. exports.routes = router;
  20. exports.products = products;
new admin.js file
  1. const path = require("path");
  2. const express = require("express");
  3. const productsController = requre('../controller/products'); // here's your new ProductsController
  4. const router = express.Router();
  5. router.get('/add-product', productsController.getAddProduct);
  6. router.post("/add-product", productsController.postAddProduct);
  7. module.exports = router;
2) shop.js (back to top...) Here's what it did look like:
const path = require("path"); const express = require("express");
1
const rootDir = require("../utility/path");
2
const adminData = require("./admin"); const router = express.Router();
3
router.get("/", (req, res, next) => { const products = adminData.products; res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); }); module.exports = router;
1
const rootDir = require("../utility/path");
You were needing this before in order to be able to properly resolve the "path" that was going to connect you to your CSS file. You don't need that anymore.
2
const adminData = require("./admin");
You were only going to need this if you're going to be processing that data on this page. Now you're not.
3
router.get
This is the syntax responsible for retrieving everything form the array stored in the "arrayData.products" variable. Now, you're referring to all that code that will be housed in the "products" Controller. Let's have a look...
const path = require("path"); const express = require("express"); const productsController = require('../controllers/products'); const router = express.Router();
3
router.get("/", productsController.getProducts); module.exports = router;
3
router.get You see? Nothing's really changed with the exception that everything now is being grabbed from another page. Here's how it looks side by side:
old shop.js
  1. const path = require("path");
  2. const express = require("express");
  3. const rootDir = require("../utility/path");
  4. const adminData = require("./admin");
  5. const router = express.Router();
  6. router.get("/", (req, res, next) => {
  7. const products = adminData.products;
  8. res.render("shop", {
  9. prods: products,
  10. pageTitle: "Shop",
  11. path: "/",
  12. hasProducts: products.length > 0,
  13. activeShop: true,
  14. productCSS: true
  15. });
  16. });
  17. module.exports = router;
new shop.js file
  1. const path = require("path");
  2. const express = require("express");
  3. const productsController = require('../controllers/products');
  4. const router = express.Router();
  5. router.get("/", productsController.getProducts);
  6. module.exports = router;
3) product.js (back to top...) Here's the "product.js" page. Let's look at it and break it down:
const products = [];
1
exports.getAddProduct = (req, res, next) => { res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product", formsCSS: true, productCSS: true, activeAddProduct: true }); };
2
exports.postAddProduct = (req, res, next) => { products.push({ title: req.body.title }); res.redirect("/") };
3
exports.getProducts=(req, res, next) => { res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); };
Most of the code should look familiar to you with the exception of how everything is being prefaced with the "exports" code. Here's the first "change" that we want to consider:
1
exports.getAddProduct = (req, res, next) => { The original code looked like this when it was parked on the original "admin.js" page: router.get("/add-product", (req, res, next) => { res.render("add-name", { pageTitle: "Add Name", path: "/admin/add-name", formsCSS: true, productsCSS: true, activeAddProduct: true }); }); It's the same code with the exception of the first line: exports.getAddProduct. This makes sense! Instead of router.get("/add-product", (req, res, next), where you've got the route followed immediately by the actual syntax, now we've got router.get('/add-product', productsController.getAddProduct);. In other words, we're identifying the route and then calling the actual functionality associated with that route by referring to what is now on our "Controller" page (product.js). That code doesn't reference the route, it just refers to the code associated with that route. Look... The first part of the exports.getAddProduct=(req, res, next) => { is "exports." "exports" is an object that, by definition, is going to be a collection of properties and methods. We're going to be packing our "exports" object with all kinds of stuff! The first thing we're going to be packing into it is a method called "getAddProduct" and that explains the very next part of the code, exports.getAddProduct. That's the pattern that applies to everything we've done as far as "placing the actual code in the "product.js" file that was in the "admin.js file and referencing it as the name of the method in the Controller file and then assigning it to the "exports" object.
2
exports.postAddProduct = (req, res, next) => { This was the "post" code that we had on "admin.js." Now, we're just reiterating the same code, calling it "postAddProduct" so the "admin.js" file knows how to refer to it in the context of the Controller dynamic (productsController.postAddProduct) and that's all there is to it!
3
exports.getProducts=(req, res, next) => { This was code that we originally had our "shop.js" page. And just for the sake of review: exports.getProducts=(req, res, next) => { res.render("shop", { // this is the "shop.js" view page prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); }; Again, the "exports.getProducts" is now the name of our method in the Controller with the path being identified on our "shop.js" file.
Remember, you didn't have a "products.js" file before we reconfigured our app to be based on an MVC architecture. That's why there's no "before / after" scenario here!
Just for the sake of establishing a sound line in the sand as far as the fact that right now we're looking at 100% accurate code, here's what we've got: app.js - this is your starting point. The change we made here was the way in which your "404" page was referred to.
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const app = express(); app.set("view engine", "ejs"); app.set("views", "views"); const adminData = require("./routes/admin"); const shopRoutes = require("./routes/shop"); const errorController = require("./controllers/error"); app.use(bodyParser.urlencoded()); app.use(express.static(path.join(__dirname, "public"))); app.use("/admin", adminData.routes); app.use(shopRoutes); app.use(errorController.get404); app.listen(3000);
admin.js - change the "POST" and "GET" functionality. This is feeding your "add-product" route.
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const productController = require("../controllers/product"); const router = express.Router(); router.get("/add-product", productController.getAddProduct); router.post("/add-product", productController.postAddProduct); exports.routes = router;
shop.js - moving the "getProducts" code over to the Controller. This is feeding your "shop" route which is your default page.
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const adminData = require("./admin"); const productController = require("../controllers/product"); const router = express.Router(); router.get("/", productController.getProducts); module.exports = router;
Here is your functionality in the "controllers" directory. There are only two files, here's the "products.js" file: products.js
const products = []; exports.getAddProduct = (req, res, next) => { // this is what shows up when you first access the pag res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product", formsCSS: true, productCSS: true, activeAddProduct: true }); }; exports.postAddProduct = (req, res, next) => { // this adds whatever you just added to the "products" array products.push({ title: req.body.title }); res.redirect("/"); }; exports.getProducts = (req, res, next) => { //this is the code that displays everything on your "shop.js" page res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); };
...and here's your "error.js" file: error.js - easy peasy...
exports.get404 = (req, res, next) => { res.status(404).render("404", { pageTitle: "Page Not Found", path: "" }); };
B) The Model (back to top...) Just for the sake of making things crystal clear. When we first stated this tutorial, we were writing things in pure, hard-core Node.js. We then added the "Express" dynamic which gives us the opportunity to "write" a lot more with less code. We then began setting up our app with the routes and the includes that made sense. We're now going back and organizing our code into the three main categories that characterize the best practices / solid designs found in quality applications: MVC - Model, View, Controller. The "app.js" file is going to be the starting point for your application by importing your libraries, registering the appropriate objects, setting your view template and defining your routes. The "routes," for all intents and purposes, are what funnels your systemic flow to the appropriate Controllers. Your Controllers will reference your Models and that's what we're doing here. 1) product.js (controller) (back to top...) Here are the changes you're going to make in your current "product.js" file in your "controllers" directory:
const Product = require("../models/product"); // import your product model exports.getAddProduct = (req, res, next) => { res.render("add-product", { pageTitle: "Add Product", path: "/admin/add-product", formsCSS: true, productCSS: true, activeAddProduct: true }); }; exports.postAddProduct = (req, res, next) => { const product = new Product(req.body.title); product.save(); res.redirect("/"); }; exports.getProducts = (req, res, next) => { const products = Product.fetchAll(); res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); };
const product = new Product(req.body.title);
You're going to start this sequence by establishing a new instance of your Product class called, "product." You're passing the "title" variable into the Class which is going to be processed here in just a little bit. A quick aside, the above code brought to mind the way in which Classes are called and methods are invoked in PHP. While variables aren't always passed into a Class declaration, it does happen and it's not a bad thing to review that real quick just to better reinforce what we're doing here in Node. Click here to expand the area below to reveal and example of a PHP Class where there are variables being passed into it.
<?php class Applied { public $word; public function __construct($greeting) {
$this->word=$greeting; } public function speak() { return $this->word; } } $buddy = new Applied("hello"); $yell = $buddy->speak(); echo $yell;
?>
1
- creating a new instance of the "Applied" class and passing the word "hello" as a variable
2
- creating the $word property
3
- you're now populating the $word property with $greeting variable which is assumed to be the first variable you passed into the Class
4
- returning the $word variable which has been populated with the $greeting value - the first value you passed into the Class.
5
- "echoing" what is going to be the word, "hello"
2
product.save();
You're calling the specific function within the "product.js" Controller and adding the title to your "product" array.
3
const products = Product.fetchAll();
Notice you don't have to create a new instance of the Product class. Reason being is because "fetchAll" is a static class so you don't have create an instance of the Class and then call the method. You can do it just like it's coded above.
2) product.js (model) (back to top...) Here's your "product" Model.
const products = []; // this was in our "controllers" file. Now, we've got it here in our Models. We're setting up an empty array. module.exports = class Product { // we're exporting this whole class constructor(title) { // we're using the constructor to define our incoming "title" string this.title = title; } save() { products.push(this); // we've got to grab the whole object and not just the "title" piece } static fetchAll() { // convenient "static" function that we can grab without having to declare an instance of the class return products; } };
Look at things side by side. On the left you have the "products.js" page when we were "cheating" a little bit and allowing both the Model and the Controller categories of functionality to exist side by side. Here's what it's going to look like when we break it up:
products.js (controller and model)
  1. const products = [];
  2. exports.getAddProduct = (req, res, next) => {
  3. res.render("add-product", {
  4. pageTitle: "Add Product",
  5. path: "/admin/add-product",
  6. formsCSS: true,
  7. productCSS: true,
  8. activeAddProduct: true
  9. });
  10. };
  11. exports.postAddProduct = (req, res, next) => {
  12. products.push({ title: req.body.title });
  13. res.redirect("/")
  14. };
  15. exports.getProducts=(req, res, next) => {
  16. res.render("shop", {
  17. prods: products,
  18. pageTitle: "Shop",
  19. path: "/",
  20. hasProducts: products.length > 0,
  21. activeShop: true,
  22. productCSS: true
  23. });
  24. };
products.js (controller only)
  1. const Product = require("../models/product"); // import your product model
  2. exports.getAddProduct = (req, res, next) => {
  3. res.render("add-product", {
  4. pageTitle: "Add Product",
  5. path: "/admin/add-product",
  6. formsCSS: true,
  7. productCSS: true,
  8. activeAddProduct: true
  9. });
  10. };
  11. exports.postAddProduct = (req, res, next) => {
  12. const product = new Product(req.body.title);
  13. product.save();
  14. res.redirect("/");
  15. };
  16. exports.getProducts = (req, res, next) => {
  17. const products = Product.fetchAll();
  18. res.render("shop", {
  19. prods: products,
  20. pageTitle: "Shop",
  21. path: "/",
  22. hasProducts: products.length > 0,
  23. activeShop: true,
  24. productCSS: true
  25. });
  26. };
Couple that with the "products.js" model and you've got yourself a happening app! BOOM!
The File System (fs) is an integral part of the Node dynamic. It give you access to the physical files on your system and is defined as being responsible for all asynchronous or synchronous file I/O (input / output) operations.
Up to now, we've been storing our data in the "products" array. Now we're going to take whatever data has been inputted and writing it to a file on our desktop. This is going to involve the "fs" or "file system" module that comes with Node. Let's pop the hood on this thing and see how it works. A) How You're Going to Change the Model (back to top...) 1) File System (fs) (back to top...)
const fs = require("fs"); const path = require("path"); const products = []; module.exports = class Product { constructor(title) { this.title = title; } save() { const p = path.join( path.dirname(process.mainModule.filename), "data", "products.json" ); fs.readFile(p, (err, fileContent) => { let products = []; if (!err) { products = JSON.parse(fileContent); } products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); }); } }
Here are the initial changes you're making to the "save" method. 1) File System (fs) (back to top...)
1
const fs = require("fs"); First thing we're going to do is import the "File System" module. 2) path (back to top...)
2
Next thing you're doing is you're using the path module. Again, by definition, this is the facility within Node that gives you the ability to work with file paths and directories. a) join (back to top...) The join property is what you're using code-wise to join two different paths together. b) dirname (back to top...) dirname is what Node uses to identify the current URL / path of the current file. process.mainModule.filename is what you use to identify the whole URL of the application you're currently running. So, when you combine the whole gambit of that one chunk of code, what you're basically doing is identifying the path of the file you're writing to. That's a lot of code, but that's what it's doing!
At one point, the tutorial references how "this" is accurately identified by the system / code because of the "fat arrow" function that's being used. At first brush that might seem a little bizarre. But when you pop the hood on the documentation pertaining to the "fat arrow" function, it becomes very clear.
  • First of all, the "fat arrow" function represents a wonderful form of shorthand so you can write more syntax with less code
  • Secondly, with that format comes a definitive way to define this
    • this is usually defined by the context it's written in, although it can be defined explicity (bound) with "bind," "call" and "apply"
  • sfat arrow functions don't bind this. Instead, it's bound according to the function it's written in (lexically).
3) readFile (back to top...)
3
readFile(p, (err, fileContent) => readFile is what Node uses to read a file on your computer. It takes two arguments: "err" and "data," or in this case, "fileContent." a) JSON.parse(fileContent) (back to top...) JSON.parse(fileContent) - the JSON module comes with Node, so you don't have to import it. On this line, you're parsing your file as a JSON object. 4) products.push(this) (back to top...)
4
products.push(this) - "this" represents the entire object that is current in the method you're in right now. In this instance, it's the parsed JSON article that you've created in the previous line. 5) fs.writeFile (back to top...)
5
fs.writeFile... this is going to either create or completely replace the file (the one that is referred to by "p" [the url of the current file]). "stringify writes the JSON object to a file. "readFile" translated your file into a JSON object and now it is writing it to a file. Take a minute and think about what you had before. save() was this: save() { products.push(this); } Now, it's everything that you see above. So, here's a "before" and "after" look:
products.js (model [storing data in array])
  1. li>const products = []; // this was in our "controllers" file. Now, we've got it here in our Models. We're setting up an empty array.
  2. module.exports = class Product { // we're exporting this whole class
  3. constructor(title) { // we're using the constructor to define our incoming "title" string
  4. this.title = title;
  5. }
  6. save() {
  7. products.push(this); // we've got to grab the whole object and not just the "title" piece
  8. }
  9. static fetchAll() { // convenient "static" function that we can grab without having to declare an instance of the class
  10. return products;
  11. }
  12. };
products.js (model [storing data in products.json])
  1. const fs = require("fs");
  2. const path = require("path");
  3. const products = [];
  4. module.exports = class Product {
  5. constructor(title) {
  6. this.title = title;
  7. }
  8. save() {
  9. const p = path.join(
  10. path.dirname(process.mainModule.filename),
  11. "data",
  12. "products.json"
  13. );
  14. fs.readFile(p, (err, fileContent) => {
  15. let products = [];
  16. if (!err) {
  17. products = JSON.parse(fileContent);
  18. }
  19. products.push(this);
  20. fs.writeFile(p, JSON.stringify(products), err => {
  21. console.log(err);
  22. });
  23. });
A) static fetchAll(); (back to top...) Right now, our fetchAll() code is very brief: static fetchAll() { return products; } It's a static function that is simply returning the "products" array which will be parsed by our "shop.ejs" file in our "views" directory. We have to change that up a bit now, because the JSON file isn't going to be read the same way an associate array would be processed. Now, we've got use "readFile."
static fetchAll() { const p = path.join( path.dirname(process.mainModule.filename), "data", "products.json" ); fs.readFile(p, (err, fileContent) => { if (err) { return[]; } return JSON.parse(fileContent); }); }
1
static fetchAll(); - it's a static function and therefore, by default, we can call it without having to create an instance of the class beforehand. 1) path.join (back to top...)
2
const p = path.join( - you're setting up "p" as a variable and letting that represent the URL of the document you're targeting on your system. You're going to "join" the path of the app you're running (process.mainModule.filename) with the "data" directory and the actual name of the doc you're reading (products.json). 2) process.mainModule.filename (back to top...)
3
path.dirname(process.mainModule.filename),( - again, a repeat of something we've gone over before. This is the tip of a glorious iceberg that helps the system identify the URL / path of the application you're currently running. It's a combo of "path.join" and this syntax which results in the complete path of the doc you're getting ready to read.
Among the data types that exist, you have strings, integers, booleans etc. JSON, by default, is a string in that it's text. You use this functionality to convert it to a JSON Object that you can then retrieve as an array. Click here for an example.
3) readFile (back to top...)
4
- we've gone over this command before. This is a Node command that allows you to read a file that's sitting in your system as opposed to something that's in a web-based directory. 4) return JSON.parse(fileContent); (back to top...)
5
return JSON.parse(fileContent) - right now, you've got some text and you need to convert that into a JavaScript object. You do that with JSON.parse
Right now, if we run this code, we get a message that says, "Cannot read property 'length' of undefined." Reason for this is that, true to form, Node (JavaScript) being the asynchronous beast that it is, will plow through all of the functions that are on its plate and not wait for one method to finish before starting on the next. This is what's happening here. The "products.json" file is represented by the "prods" variable in your Controller and that's going out the door before the system has a chance to access the "products.json" file on your Model. It's here where the utility of a "callback" becomes both necessary and glorious. By definition, a Callback" is an argument that you pass into a function. At least, that's as far as we've gone with it up till now. First, however, for the sake of review, let's revisit the difference between Classes, Methods and Instances...

PHP Classes | Objects | Instances

C) Callback - The Sequel Also, as a quick "pop the hood" kind of moment: What is a "callback?" This was covered to a certain extent earlier, however... JavaScript is "asynchronous." That means that while the code is being read by the system in the manner that you would expect, as far as it being processed top to bottom, it doesn't wait for a function to wait before it moves on to the next block of code. In some cases, that's good because you want to keep things moving. But it can also be a problem if the next block of code depends on a result that's coming from the previous piece of syntax. In other words, if you're going down the line and you suddenly enounter a function that

Javascript Callbacks, Hoisting and Anonymous Functions

is expecting something that has yet to be calculated by a chunk of code that has yet to finish running, you're going to have a problem. That's where "callback" becomes so helpful and, in some instances, so necessary. In addition to what we've talked about beforehand, know this:Now, let's take a look at what we've got with this exercise and take it apart.
Here is where we need to take a little tangent and talk about Event Emitters. Node is defined as being an "event driven" architecture. An "Event" is simply a signal that something has happened. It's actually a class within Node that has a substantial amount of functionality attached to it. We've already seen this in action, although the actual code was replaced with some Express syntax. When we first started, we did this: const server = http.createServer(app); When we invoked this, Node recognizes this as an "Event" - there's a request that necessitates a response. This is where you get the "Event driven" dynamic from in Node's definition. Pause here for a moment and recognize that you've been doing Event Driven programming your entire career. In the context of a Web Browser a "click" is an event. When you press a key, you're triggering a "keydown" event. By default, at its most basic level, Event Driven Programming is based on the idea that an Event Handler (something that's clicked or pressed, for example), is a call back function that will be called when an event is triggered. If you go to the Node documentation and take a look, you'll set that "Events" is a module with a entire block of functionality attached to it. One of those pieces of functionality is something called an "Event Emitter." An "Event Emitter" is basically something that's making a noise - it's an alert that something is happening. It doesn't do anything in response to that noise, for that you need a Listener. But that's what an Event Emitter is.
1) inner function (back to top...) Here's what we had and what we're going to use now: static fetchAll() { const p = path.join( path.dirname(process.mainModule.filename), "data", "products.json" ); fs.readFile(p, (err, fileContent) => { if (err) { return[]; } return JSON.parse(fileContent); }); } There are two things going on here that are worth mentioning. First, fs.readFile is, by default a "callback." Remember, fs is the "File System" module within Node. readFile is a function within that module that's going to take some time to process, hence it is a "callback." In addition, fs.readFile is a function within a function and is therefore referred to as an "inner function." Having an "inner function" is not a big deal, but in this case it's problematic because everything that's being returned is coming from the inner function. That's fine, but we're calling fetchAll and because you don't have a "return" that corresponds to that particular part of the function, nothing's being returned. So, here's how we fix all of this... What we started with is on the left and what we have now with the callback is on the right.
products.js (model [before cb])
static fetchAll() { const p = path.join( path.dirname(process.mainModule.filename), 'data', 'products.json' ); fs.readFile(p, (err, fileContent) => { if(err) { return[]; } return JSON.parse(fileContent); }); }
products.js (model [with cb])
static fetchAll(cb) { const p = path.join( path.dirname(process.mainModule.filename), 'data', 'products.json' ); fs.readFile(p, (err, fileContent) => { if(err) { cb([]); } cb(JSON.parse(fileContent)); }); }
Remember, the initial problem is that you've got a callback as an inner function that's not returning anything. To remedy that problem, we're going to accept a function as an argument that will now return something - be it an empty array or an actual result. Either way, we're going to have something our code can measure as far as it's length. That's our Model. Let's look at the way we're now calling this from our Controller.
products.js (controller [before cb])
exports.getProducts = (req, res, next) => { const products = Product.fetchAll(); res.render('shop, { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); }
products.js (controller [with cb])
exports.getProducts = (req, res, next) => { Product.fetchAll(products => { res.render('shop', { prods: products, pageTitle: 'Shop', path: '/', hasProducts: products.length > 0, activeShop: true, productCSS: true }); }); }
The Model is now expecting a function (cb). That's how we're dictating the flow of how things are going to be processed. Before, "products" was coming up empty because it was tied to an inner function that was a callback. As a result, the system had already asked for "products" before "products" could be gauged. Now, we're structuring things in a way where "products" is going to be defined by either an empty array or JSON.parse(fileContent). To visualize it, think of it this way: When you've got cb([]);, what you actually have is: [] => { res.render('shop', { prods: [], pageTitle: 'Shop', path: '/', hasProducts: products.length > 0, activeShop: true, productCSS: true }); ...and when you don't have an "err," you've got this: JSON.parse(fileContent) => { res.render('shop', { prods: JSON.parse(fileContent), pageTitle: 'Shop', path: '/', hasProducts: products.length > 0, activeShop: true, productCSS: true }); When you're looking at this function: products => { res.render('shop', { prods: products, pageTitle: 'Shop', path: '/', hasProducts: products.length > 0, activeShop: true, productCSS: true }); ...it's tempting to see products as the name of the function. It isn't. It's an anonymous function and "products" is actually your argument. So, when you hit the "products.js" model file and you see "cb(something...)," what you're seeing is the above function and "products" is really nothing more than a placeholder. In this example, it's going to be either "[]" or "JSON.parse(fileContent). The bottom line is that entire function is expecting "something" in the place of the "products" variable. A) Helper Function (back to top...) The first thing we're doing is adding a Helper Function. For those of you who don't know what a Helper Function is, it looks like this: A Helper Function is going to "assist" another function. Here's a basic example:
<script> function helper(number) { return help(number+5); } function help(digit) { console.log(digit) } helper(5); </script>
What you're doing here is basically building on the foundation established by another function. You can see that dynamic played out with helper(5). The helper function, rather than returning a generic result, actually returns the help function with a little extra syntax thrown in. While this particular example is so simplistic that the "helper" dynamic doesn't seem to be that much of a "help," imagine if the help function was extremely complex. At that point, the helper becomes a real Godsend in the way you can add some incremental elements without having to reproduce the entire help function from scratch. The reason this is significant is because our current code in our Model has some redundancies. In other words, you've got some repetition and when you have that, you want to do some refactoring so it's more streamlined. Not just from the standpoint of aesthetics, but it becomes much easier to troubleshoot for the guy coming after you. Our current code is on the left and our "new and improved" code is on the right:
products.js (model [before helper function]
const fs = require("fs"); const path = require("path"); module.exports = class Product { constructor(t) { this.title = t; } save() { const p = path.join( path.dirname(process.mainModule.filename), "data", "products.json" ); fs.readFile(p, (err, fileContent) => { let products = []; if (!err) { products = JSON.parse(fileContent); } products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); }); } static fetchAll(cb) { const p = path.join( path.dirname(process.mainModule.filename), "data", "products.json" ); fs.readFile(p, (err, fileContent) => { if (err) { return []; } cb(JSON.parse(fileContent)); }); } };
products.js (model [with helper])
const fs = require("fs"); const path = require("path"); const p = path.join( path.dirname(process.mainModule.filename), "data", "products.json" ); const getProductsFromFile = cb => { fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); }; module.exports = class Product { constructor(t) { this.title = t; } save() { getProductsFromFile(products => { products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); }); } static fetchAll(cb) { getProductsFromFile(cb); } };
When you compare the original with the new and improved, a couple of things stand out.
1
we establish the "p" constant as a global variable so it doesn't have to be declared and defined every time it's used. Before we move on in our assessment of how things compare before and after the "helper" function is being used, remember that the function expression being passed to the fetchAll function is actually defined in our controller. So, when you see "cb" in this context, don't be confused by the absence of: products => { res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); That's in the "products.js" (controller) file.

Helper Functions
2
This is our "helper" function. This is what was introduced as a way to streamline our code and avoid what would otherwise be some redundancy. The "helper" function is now assuming all of what was originally happeing in your fetchAll function which looked like this: static fetchAll(cb) { const p = path.join( path.dirname(process.mainModule.filename), "data", "products.json" ); fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); } Now, it looks like this: static fetchAll(cb) { getProductsFromFile(cb); } Head out to the Captain's Log for more info.
3
We're using the same stuff that we did before, only we're passing a slightly different expression into our callback. Take a look at the two expressions as they're presented side by side so you can better appreciate and understand the differences.
expression for "push"
products => { products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); }
...and that's the expression being passed in as a callback to "getProductsFromFile"... const getProductsFromFile = cb => { fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); };
expression for fetchAll
products => { res.render("shop", { prods: products, pageTitle: "Shop", path: "/", hasProducts: products.length > 0, activeShop: true, productCSS: true }); }
...and that's the expression we're passing in to "getProductsFromFile" when we're just retrieving the list of products in our "products.json" file... const getProductsFromFile = cb => { fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); };
The key to understanding this is to remember that "products" is a variable. If you try to process it as the name of a function, it will throw you off. The way that this is being interjected looks like this: // here's your expression... (products) => { products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); } A) Add a Route (back to top...) We're going to start by adding some additional pages. First thing, then, is going to be adding some additional routes. Here's an example: router.get("/orders", shopController.getOrders); So, the URL is going to be "/orders." Then you've got the "shop.js" file located in the "Controllers" directory and we're looking for a method called, "getOrders." B) Add a Method to the Controller (back to top...)
exports.getOrders = (req, res, next) => { res.render("shop/orders", { path: "/orders", pageTitle: "Your Orders" }); };
Cake and Ice Cream, baby! This is your "getOrders" method that's going to render the "shop/orders" view. The code passes the path and the pageTitle as variables into the View directory. The system understands the above as the "orders.js" file located in the "view/shop" directory. C) Basic HTML vs Content (callback review) (back to top...) 1) Basic HTML (back to top...) Let's revisit the "product-list.js" file in the "views/shop" directory. That content is coming from the "getProducts" method in the "shop.js" controller. If "product-list.js" was nothing but basic HTML, the "getProducts" method would look something like this: exports.getProducts = (req, res, next) => { res.render("shop/orders", { path: "/orders", pageTitle: "Your Orders" }); That's what we just did in the previous section. But we're not doing just HTMLs. Instead, we're grabbing our content from the "products.json" file. All of that content is being retrieved through the "products.js" file in the "models" directory. 2) Dynamic Content (back to top...) Here's the router that's dictating the content of our user's page based on the path they've selected... router.get("/products", shopController.getProducts); a) Controller (back to top...) That's going to trigger the "product.js" file in the Controller directory and it's going to hit the "getProducts" method which looks like this: const Product = require("../models/product"); exports.getProducts = (req, res, next) => { Product.fetchAll(products => { res.render("shop/product-list", { prods: products, pageTitle: "All Products", path: "/products" }); }); }; Notice the "product.js" file in the "models" directory referenced at the top. Notice also the way in which everything that's in bold is a function that's being passed into the "fetchAll" method as an argument. That, by definition, is a callback. static fetchAll(cb) { getProductsFromFile(cb); } The "cb" is the function: products => { res.render("shop/product-list", { prods: products, pageTitle: "All Products", path: "/products" }); } Remember, this is an anonymous function written in the ES6 format. Normally, it would be written like this: function(products) => { res.render("shop/product-list", { prods: products, pageTitle: "All Products", path: "/products" }); } This is so incredibly important, it has to be put in its own text box!
"products" is a variable! The expression is distinct from the variable. Everything from the fat arrow on represents something distinct from that variable.
Our fetchAll method is passing that function into the getProductsFromFile method. const getProductsFromFile = cb => { fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); }; What's in bold is the (products) variable. b) View (back to top...) Here's where it really starts making sense: Thanks to this: function(products) => { res.render("shop/product-list", { prods: products, pageTitle: "All Products", path: "/products" }); } ...and the way that it was positioned as a callback, "products" is now a bonafide value that is now being read into the "shop/product-list" view file...
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/product.css" /> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <h1>My Products</h1> <p>List of all the products...</p> <% if(prods.length>0) { %> <div class="grid"> <% for (let product of prods) { %> <article class="card product-item"> <header class="card__header"> <h1 class="product__title"><%= product.title %></h1> </header> <div class="card__image"> <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book" /> </div> <div class="card__content"> <h2 class="product__price">$19.99</h2> <p class="product__description"> A very interesting book about so many even more interesting things! </p> </div> <div class="card__actions"> <button class="btn">Add to Cart</button> </div> </article> <% } %> </div> <% } else { %> <h1>No Products Found</h1> <% } %> </main> <%- include('../includes/end.ejs') %>
Notice how it's looking for "products...?" <% if(prods.length>0) { %> Again, refer to the Captain's Log page to get a refresher. A) Add Button (back to top...) To add the Detail Button, we'll go into your "product-list.ejs" file in the Views directory and do this:
<a href="/products/<%=product.id %>" class="btn">Details</a> <form action="/add-to-cart" method="POST"> <button class="btn">Add to Cart</button> </form>
The <%=product.id %> element is going to come from what represents a "new and improved" way of creating an ID for every product we add. That's going to look like this... B) Add ID (back to top...) To add an ID to our existing product, we'll do this in the "products.js" file in the "models" directory:
module.exports = class Product { constructor(title, imageUrl, description, price) { this.title = title; this.imageUrl = imageUrl; this.description = description; this.price = price; } save() { this.id = Math.random().toString(); getProductsFromFile(products => { products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); }); } static fetchAll(cb) { getProductsFromFile(cb); } };
We defined all of the basic elements in the context of our constructor. Now we'll ad the Math.random().toString() piece to what is the "products" variable and that's how we'll save our stuff to what will be a more elaborate "products.json" file. It now looks like this: [ { "id": "7747", "title": "Muscular Christianity", "imageUrl": "http://muscularchristianityonline.com/forum/wp-content/uploads/2017/04/book_display.jpg", "description": "A great workout program!", "price": "10.99" } ] With the "id" in place, we now have the ability to display a specific file and that's what the <%=product.id %> syntax is carrying! C) Retrieve ID (back to top...) 1) The Order of Your Routes (back to top...) Here's what you're going to add to your routes.js file: router.get('/products:productID'); Notice the ":productID" part. That's your dynamic content. That's what you're going to use to retrieve the value that will show up in that space. One thing to keep in mind is that you want to position this route correctly. The system reads things top to bottom. If you place ":productID" at the top, your page won't be routed appropriately. For example, if you did this: router.get('/products:productID'); router.get('/products/delete'); ...you would never fire "/products/delete" because the system would treat the dynamic content as "delete" so you would never get to that point. That being the case, you always want to put your dynamic content last. A) The Controller (back to top...) Here's the code you're using in the Controller... exports.getProduct = (req, res, next) => { const prodId = req.params.productId; Product.findById(prodId, product => { console.log(product); }); res.redirect("/"); }; The new element is the "findById" method. You've got two arguments: One is the "prodId," which we've already retrieved and then we've got a function we're passing in as a callback. Basically, what we're doing with the callback is just publishing all of our info about the product on to the console. B) The Model (back to top...) Here's the code as it's being drafted in the Model... static findById(id, cb) { getProductsFromFile(products => { const product = products.find(p => p.id === id); cb(product); }); } What's in bold is an abbreviated IF statement. That's something you want to be aware of. Also, in findById, you've got two parameters; an id and a callback. And then you're passing a function as a parameter into the getProductsFromFile function, so you've got a couple of dynamics working siumultaneously. The getProductsFromFile looks like this: const getProductsFromFile = cb => { fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); }; That structure is going to return all of the data that corresponds to every product that's in the "products.json" file. So, now "products" as it's documented in the method is being "mined" by this: const product = products.find(p => p.id === id); ...and const product becomes the array that corresponds to the id that we intitially grabbed and all that is being posted to the console with: Product.findById(prodId, product => { console.log(product); }); C) The View (back to top...) Here's what we're going to use in our Controller to trigger and display the "product-detail.ejs" file for our View: exports.getProduct = (req, res, next) => { const prodId = req.params.productId; Product.findById(prodId, product => { res.render("shop/product-detail", { product: product, pageTitle: product.title, path: "/products" }); }); }; The product variable is now a full fledged array loaded with data pertaining to the product represented by the product id in the URL. All of that is coming from... static findById(id, cb) { getProductsFromFile(products => { const product = products.find(p => p.id === id); cb(product); }); } ...in our Model. We pass the "pageTitle" and "path" like we did with the other views and we're gold! We've gone over how we're going to facilitate the "Product Detail" page by including the "id" in the URL so we can grab that and then display the corresponding product appropriately. Now we need to so something similar when we add a product to the Cart. To do that, you need to think of two scenarios:Here's how you're going to pull those two situations off... A) Add to Cart (back to top...) To add an item to the cart, you're going to "post" the ID of that product to the "cart" page. That being the case, it's going to look like this: 1) The View (back to top...) We'll start with the View. Here's how we're going to capture the ID of the product we're getting ready to add to the Cart... <form action="/cart" method="POST"> <button class="btn">Add to Cart</button> <input type="hidden" name="productId" value="<%=product.id %>" /> </form> Look at the "action." We're posting our data to the "cart" page, so let's set up your route... 2) The Route (back to top...) "/cart" is our route, so... router.post("/cart", shopController.postCart); ...and then we'll need a Controller and a method called "postCart." We don't have those built yet, but that's going to be the flow, so let's set up those elements in our Controller now. 3) The Controller (back to top...) We were flying by the seat of your pants a moment ago and just naming the Controller, knowing that we were going to need one. So, we're in the "shop.js" file and we're going to add a method called, "postCart." Here's how that's going to look: exports.postCart = (req, res, next) => { const prodId = req.body.productId; console.log(prodId); // for training purposes res.redirect("/cart"); }; This line: const prodId = req.body.productId; is what we use when we're wanting to grab some data that's being "posted" to a page. "productId" is this form that we just set up in our View a moment ago. <form action="/cart" method="POST"> <button class="btn">Add to Cart</button> <input type="hidden" name="productId" value="<%=product.id %>" /> </form> B) Add to Cart as an Include (back to top...) You can use the above code throughout your app by positioning it as an include. So, we'll add: <form action="/cart" method="POST"> <button class="btn">Add to Cart</button> <input type="hidden" name="productId" value="<%=product.id %>" /> </form> ...to our "product-list.ejs," "product-detail.ejs" and the "index.ejs" pages. One thing to keep in mind... When we assert this code as an include - like this: <%- include('../includes/add-to-cart', {product: product})%> We've got remain congnizant of the fact that an include doesn't pick up the "product" object like we can when we're writing straight code in the context of a loop. So, this:
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/product.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <% if (prods.length > 0) { %> <div class="grid"> <% for (let product of prods) { %> <article class="card product-item"> <header class="card__header"> <h1 class="product__title"> <%= product.title %> </h1> </header> <div class="card__image"> <img src="<%= product.imageUrl %>" alt="<%= product.title %>"> </div> <div class="card__content"> <h2 class="product__price">$ <%= product.price %> </h2> <p class="product__description"> <%= product.description %> </p> </div> <div class="card__actions"> <a href="/products/<%=product.id %>" class="btn">Details</a> <form action="/cart" method="POST"> <button class="btn">Add to Cart</button> <input type="hidden" name="productId" value="<%=product.id %>" /> </form> </div> </article> <% } %> </div> <% } else { %> <h1>No Products Found!</h1> <% } %> </main> <%- include('../includes/end.ejs') %>
...will work just fine. However, when we attempt to use our "include" dynamic like this:
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/product.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <% if (prods.length > 0) { %> <div class="grid"> <% for (let product of prods) { %> <article class="card product-item"> <header class="card__header"> <h1 class="product__title"> <%= product.title %> </h1> </header> <div class="card__image"> <img src="<%= product.imageUrl %>" alt="<%= product.title %>"> </div> <div class="card__content"> <h2 class="product__price">$ <%= product.price %> </h2> <p class="product__description"> <%= product.description %> </p> </div> <div class="card__actions"> <a href="/products/<%=product.id %>" class="btn">Details</a> <%- include('../includes/add-to-cart')%> </div> </article> <% } %> </div> <% } else { %> <h1>No Products Found!</h1> <% } %> </main> <%- include('../includes/end.ejs') %>
...will not work. Reason being is that because you're using an include, the "product" object doesn't get passed into the "include" automatically. You have to pass the "product" object into the include as an object. You'll get a "product is not defined" error message. To remedy that, you need to adjust your "include" syntax by passing your "product" object in as an argument like this: <%- include('../includes/add-to-cart', {product: product})%>, Now you're gold! A) Laying Down Some Basics (back to top...) To get our new "Cart" model up and running, let's review some basics. 1) const fs (back to top...) First of all, we're going to import our "fs" object as well as our "file path." const fs = require("fs"); // the "file system" object is something that's part of Node. Click here for review 2) const p (back to top...) Now, bring in your "path" object. Again, this is from the Node library. const path = require("path"); Now define that path so that it's pointing to your "cart.json" file. const p = path.join( // we're copying this syntax from our "product.js" model path.dirname(process.mainModule.filename), "data", "cart.json" // new data file ); B) Adding a Product to Your Cart (exploded view) (back to top...) 1) err -> callback (back to top...) static addProduct(id) { //fetch the previous cart fs.readFile(p, (err, fileContent) => { if(!err) { cart = JSON.parse(fileContent); } }); } We've gone through callbacks now, so we can better recognize the format of what we've got above. In this first line, you've got "readFile" and then you're passing a callback after the "p" (path) argument. This is "stock" Node, so there's no real cause for concern, as far as how you're going to code this because it's pretty standard. The first part is the "err," which stands for "error." In this instance, it will indicate whether or not we have a "cart" already in place. Now, let's get serious... 2) fetching, analyzing, adding -> exploded view (back to top...) Here's our "addProduct" method broken down and explained in its various parts. Be aware of the "find" function as well as the "spread operator."
static addProduct(id, productPrice) { //fetch the previous cart fs.readFile(p, (err, fileContent) => { let cart = { products: [], totalPrice: 0 }; if (!err) { cart = JSON.parse(fileContent); } //analyze the cart => find existing product const existingProductIndex = cart.products.findIndex( prod => prod.id === id ); const existingProduct = cart.products[existingProductIndex]; let updatedProduct; //add new product / increase quantity if (existingProduct) { updatedProduct = { ...existingProduct }; updatedProduct.qty = updatedProduct.qty + 1; cart.products = [...cart.products]; cart.products[existingProductIndex] = updatedProduct; } else { updatedProduct = { id: id, qty: 1 }; cart.products = [...cart.products, updatedProduct]; } cart.totalPrice = cart.totalPrice + +productPrice; fs.writeFile(p, JSON.stringify(cart), err => { console.log(err); }); }); }
The following is a phenomenal explanation of the difference between "var," "let" and "const." "var" reigned as king until the advent of ES6. Now, you've got access to "let" and "const" which remedy some of the shortcomings of "var." Click here to read the whole article...
Scope essentially means where these variables are available for use. var declarations are globally scoped or function/locally scoped. It is globally scoped when a var variable is declared outside a function. This means that any variable that is declared with var outside a function block is available for use in the whole window. var is function scoped when it is declared within a function. This means that it is available and can be accessed only within that function. To understand further, look at the example below... (click here to read more)
1
fs.readFile(p, (err, fileContent) => { let cart = { products: [], totalPrice: 0 }; if (!err) { cart = JSON.parse(fileContent); } With this first piece of the method, you're reading the "cart.json" file which is represented by the "p" which was defined earlier on the page as the path to the "cart.json" file. let cart = { products:[], totalPrice:0 } is the "cart.json" file if there's an error, which will be triggered if there is no file. At that point, "cart" will be that object consisting of the "products array" property and the "totalPrice" property. If there is no error - if there is a "cart.json" file - then we'll invoke the JSON.parse method which will convert whatever it is we're looking at into a JSON object which we can then read for the sake of functionality we'll need down the road.
BTW: "cart" is an object. "let" is used to defined variables, but an "object" can be a variable to with multiple values. Click here to read more.

findIndex()
2
you're setting up a variable called "existingProductIndex" and we're going to loop through all of the products in the current "products" array and see if the product id number of the resource we're getting ready to add to our cart already exists. If it does, we'll get the "index" or the specific location within the array that the product id exists. i) find function (back to top...) Before we get into the "findIndex" function, here's the JavaScript "find" function. It looks like this: var array1 = [5, 12, 8, 130, 44]; var found = array1.find(function(element) { return element > 10; }); console.log(found); // expected output: 12 We're writing "function(element)" as an arrow function so the way this is going to be broken down is akin to this: for(products as prod) { if(prod.id=id) { //we've got a match } else { //undefined... } } Now, given that background, ES6 allows you to write the same kind of thing with less code. Let's take a look at it in the context of needing to find, not just if the value exists, but where specifically does the value show up in the array. In other words, what is the index of the value in the current array. That code is going to look like this: let arr = ['a','b','c']; arr.findIndex(k => k=='b'); // 1 Don't be too distracted by the syntax itself. Just know what's happening and what's being returned.
3
const existingProduct = cart.products[existingProductIndex]; - with the correct index identified, we can now hone in on the precise product in the "cart.json" file that we're getting ready to edit if that is, in fact, the product that has been selected.
4
set up a new variable representing the updated cart. Not only do you have an id and a description etc., you also have a "quantity" property. That brings us to something called the "spread operator."

Spread Operator
ii) spread operator (back to top...)
5
We're going to use the next generation (ES6) spread operator to create a new object with all the same properties as an already existing object. You can click on the aforementioned link or on the "Captain's Log" graphic to get some more detail.
6
updatedProduct.qty = updatedProduct.qty + 1; - we've got an "existing product." In other words, the product the user has clicked on already exists in the "cart.json" file. In order to update that, we started by creating a copy of the "existingProduct" array which is the array of elements corresponding to the productId of the product the user just selected. By using the "spread operator," we've got a copy of that array, as far as the indexes and the values. Now we're going to update the "qty" index of that array by 1.
7
cart.products = [...cart.products]; cart.products[existingProductIndex] = updatedProduct; We created a copy of the "existingProduct" array in order to update it. Now we're going to create a copy of the entire "cart" array so we can update it with the new products array that's just been updated. iii) concatenate array (ES6 using spread operator) (back to top...)
8
updatedProduct = { id: id, qty: 1 }; cart.products = [...cart.products, updatedProduct]; We're now looking at a scenario where there wasn't a product in the "cart.json" file that matched the product the user clicked on. So, we're going to establish a new "updatedProduct" object with an id property and a qty property. Then we're going to use the spread operator again to create a copy of the "cart.products" object (which is an array). We're going to update that array with the values represented by the "updatedProduct" object. When you get to cart.products = [...cart.products, updatedProduct];, you're using the "spread operator" to duplicate the structure of the current products array which is the first property in your cart object. You're then using the ES6 version of array concatenation to update all of the values in the "cart.products" array to whatever you have in the "updatedProduct" array. Your "cart" object consists of two properties: The first one is "products" which is an empty array that has two values in it: "id" and "quantity." The second property is "totalPrice" which has a value of 0. The "price" and "id" of the product being added to the cart is represented by the two arguments coming in as part of the overarching "addProduct" method.
9
cart.totalPrice = cart.totalPrice + +productPrice; fs.writeFile(p, JSON.stringify(cart), err => { console.log(err); }); We're now finished processing the two scenarios we're going to contend with and we're wrapping up now by updating the "totalPrice" which is coming to us from the original method: static addProduct(id, productPrice) ...and the "cart" object that's been edited in the context of the "products" object / array. The part that you see in bold is a change that we made to the "productPrice" variable so it doesn't get processed as a string. We finish by writing to the "cart.json" file the new information as it's been processed by the "addProduct" method! We're going to set things up in such a way where we can edit our products. A) Router (back to top...) Start by setting up our route knowing that we're going to have some dynamic content involved, hence the semicolon: router.get("/edit-product/:productId", adminController.getEditProduct); B) Controller (back to top...)
exports.getEditProduct = (req, res, next) => { const editMode = req.query.edit; if (!editMode) { return res.redirect("/"); } res.render("admin/edit-product", { pageTitle: "Edit Product", path: "/admin/edit-product", editing: editMode }); };
1) "Edit" Query Parameter (back to top...)
1
we're setting up a constant that will capture that part of the URL that designates it as an "edit" route. The actual URL is going to look something like this: admin/edit-product/7747?edit=true "req.query.edit" is grabbing the "edit" parameter. This is our "query parameter." And just for the sake of correct verbiage...
2
if the "edit" value is either nonexistent or it returns false, then the user is redirected to the index page. Otherwise you're routed to the "edit-product" view. C) View (back to top...) Our "view" is already established for the most part having copied "add-product.ejs" and made it "edit-product.ejs" and then burning our ships and eliminating "add-product" altogether. The one thing we're going to add now, however, is a little "IF" clause that will capture the "editing" variable that's being passed to our view via the Controller. I'm talking about this: editing: editMode Now, when we get to the View, we've got this little gem that's going to display the right button... <button class="btn" type="submit"><% if (editing) { %> Update Product<% } else { %> Add Product <% } %></button> Cool! A) Controller (back to top...)
exports.getEditProduct = (req, res, next) => { const editMode = req.query.edit; if (!editMode) { return res.redirect("/"); } const prodId = req.params.productId; Product.findById(prodId, product => { if(!product) { return res.redirect('/'); } res.render("admin/edit-product", { pageTitle: "Edit Product", path: "/admin/edit-product", editing: editMode, product: product }); }); };
Most of what you see above has already been covered. The material that rates further explanation / reinforcement is below.
1
the "productId" is the variable that we established in our route: router.get("/edit-product/:productId", adminController.getEditProduct);
2
"Product" is our model which we referenced earlier on the page. We're grabbing the "findById" method and passing two arguments into the function. One being the "prodId," the other being a callback that captures the array of data corresponding to our "prodId" that we're going to retrieve thanks to the "findById" method. Let's take another look at that "callback" anomaly. Here's the "findById" method: static findById(id, cb) { getProductsFromFile(products => { const product = products.find(p => p.id === id); cb(product); }); } You'll notice that it is also incorporating a "callback" dynamic as far as the "getProductsFromFile" method. That looks like this: const getProductsFromFile = cb => { fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); }; So, in a couple of sentences: You hit findById and you've got the id of your product and the function that's going to feed your view as a callback. getProductsFromFile is a constant that fires code that returns the contents of the file referenced by the "p" path as "products." The next line of that coce sets up "product" as the variable that will store the info pertaining to the selected product id (id) and that information will be the argument that feeds into the findById's callback which is the code that feeds the view. And that's that!
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/forms.css"> <link rel="stylesheet" href="/css/product.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <form class="product-form" action="/admin/<% if (editing) { %>edit-product<% } else { %>add-product<% } %>" method="POST"> <div class="form-control"> <label for="title">Title</label> <input type="text" name="title" id="title" value="<% if (editing) { %><%=product.title%><% } %>"> </div> <div class="form-control"> <label for="imageUrl">Image URL</label> <input type="text" name="imageUrl" id="imageUrl" value="<% if (editing) { %><%=product.imageUrl%><% } %>"> </div> <div class="form-control"> <label for="price">Price</label> <input type="number" name="price" id="price" step="0.01" value="<% if (editing) { %><%=product.price%><% } %>"> </div> <div class="form-control"> <label for="description">Description</label> <textarea name="description" id="description" rows="5"><% if (editing) { %><%=product.description%><% } %> </textarea> </div> <button class="btn" type="submit"><% if (editing) { %> Update Product<% } else { %> Add Product <% } %></button> </form> </main> <%- include('../includes/end.ejs') %>
1
using some dynamic content to dictate the flow of the page - whether we're adding a product or editing one
2
using <%= to render the elements located in the "product" array being served to us by our findById method in our "product.js" model
3
using some dynamic content to define whether or now we're adding a product or editing one. A) product.ejs (back to top...) This is pretty easy. All you're going to do is use EJS to write... <a href="/admin/edit-product/<%=product.id%>?edit=true" class="btn">Edit</a> That will do the trick! A) Router (back to top...) In no particular order, here's how our router is going to look: router.post("/edit-product", adminController.postEditProduct); We're not having to worry about any dynamic content in the route since everything is going to be "posted." Let's take a look at what your Controller is going to do. B) Controller (back to top...)
exports.postEditProduct = (req, res, next) => { const prodId = req.body.productId; const updatedTitle = req.body.title; const updatedimageUrl = req.body.imageUrl; const updatedDesc = req.body.description; const updatedPrice = req.body.price; const updatedProduct = new Product( prodId, updatedTitle, updatedimageUrl, updatedDesc, updatedPrice ); updatedProduct.save(); res.redirect("/admin/products"); };
1
You're going to start by capturing all of your incoming posted data. That's going to include the "prod.id" value which we incorporated into our "edit-product.ejs" file with this: <% if(editing) { %> <input type="hidden" name="productId" value="<%=product.id %>"> <% } %>
2
Next, you're going to utilize the functionality represented in the "products.js" model file (located in the "models" directory). You have a class called "Product" and with this one line of code you're instantiating a new Product object with all of the data (prodId, Title, etc) matching the constructs in the "save" method (we'll look at all that detail in a moment).
3
calling the "save" method that's now associated with the "updatedProduct" object which is an instance of the "Product" class.
4
redirecting to the "admin/products" page after you're all done where you're going to see the results of the edits you just made. C) Model (back to top...) Here's the Product Class along with the "save" method.
module.exports = class Product { constructor(id, title, imageUrl, description, price) { this.id = id; this.title = title; this.imageUrl = imageUrl; this.description = description; this.price = price; } save() { getProductsFromFile(products => { if (this.id) { const existingProductIndex = products.findIndex( prod => prod.id === this.id ); const updatedProducts = [...products]; updatedProducts[existingProductIndex] = this; fs.writeFile(p, JSON.stringify(updatedProducts), err => { console.log(err); }); } else { this.id = Math.random().toString(); products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); } }); } static fetchAll(cb) { getProductsFromFile(cb); } static findById(id, cb) { getProductsFromFile(products => { const product = products.find(p => p.id === id); cb(product); }); } };
1
"exports" is something that shows up everywhere. For the sake of making sure we've got a comprehensive appreciation for what that term means, know that it's just a very efficient way of bundling up all the functionality on that one page and delivering it in one, tight little package. Click here for more information.

function __constructor
1) construct
2
You've instantiated a new Class and you have within that class a series of constructs. A constructor is something you've seen before in PHP. It's not an uncommon thing at all. A "constructor" is a digital mechanism that's used to create variables that can be used by all the methods within that Class. Click on the "Captain's Log" graphic to the right to see it in more detail. Bottom line: You're populating those constructs with the variables that are coming from form fields. Bear in mind that on your Controller, you instantiated the Class. You've done more than create a variable containing an array of information. updatedProduct.save(); is an instance of the Product Class, so you can now use it to call the "save" method. Pretty cool, actually, in that you're not only calling the "save" method, but you're also populating those constructs with some practical data.
3
So you've called the "save" method. You've populated the previous constructs with data and that's going to be handy in just a minute when you use "prod.id" to determine whether or not you're adding a product or updating one. But before we get to that, let's pause and appreciate the flow of the syntax in general. Right out of the chute you're invoking the functionality attached to the "getProductsFromFile" const. That looks like this: const getProductsFromFile = cb => { fs.readFile(p, (err, fileContent) => { if (err) { return cb([]); } cb(JSON.parse(fileContent)); }); }; That's going to replace the "products" variable I've got highlighted in bold with either a blank array or the parsed content coming from the path leading to the "products.json" file.
getProductsFromFile(products => { if (this.id) { const existingProductIndex = products.findIndex( prod => prod.id === this.id ); const updatedProducts = [...products]; updatedProducts[existingProductIndex] = this; fs.writeFile(p, JSON.stringify(updatedProducts), err => { console.log(err); }); } else { // this is only going to be triggered if you're adding a product this.id = Math.random().toString(); products.push(this); fs.writeFile(p, JSON.stringify(products), err => { console.log(err); }); } });
4
if(this.id) - we're using this to determine the flow of the "save" method. If there's an id present (because of the id that's coming from the form), then we're going to be in "edit" mode. Otherwise, we're adding a new product.
5
findIndex is a JavaScript code that's going to return the first index in an array that matches the search criteria. Click here for more information.
6
we're using the spread operator to create an "edit-able" copy of the "products" array that's been returned by the "getProductsFromFile" method. We've talked about this before and you can review it by clicking here.
7
after having created an "edit-able" version of the "products" array and specifying which specific record within that array we want to target (existingProductIndex), we replace it with this which is going to be all of the info we gathered with the initial constructs. And that's that! A) Router (back to top...) Time to get rid of a product, both from the "product.json" file and the "cart.json" file. First, let's set up our route... router.post("/delete-product", adminController.postDeleteProduct); Since we're "posting," we won't need to concern ourselves with any dynamic content in the route. BTW: When we write our routes, fact is, we've already constructed some of the Controller which is why you'll see that piece already documented. Here's the Controller: B) Controller (back to top...) exports.postDeleteProduct = (req, res, next) => { const prodId = req.body.productId; Product.deleteById(prodId); res.redirect("/admin/products"); }; Pretty straight forward. We're grabbing our product id and sending that as "prodId" to the "deleteById" method in the "product.js" file in the "models" directory. C) Model (back to top...) We've got two "models" to consider in that we're deleting a product from the "products.json" file and the "cart.json" file. 1) product.js (back to top...) Here we go:
static deleteById(id) { getProductsFromFile(products => { const product = products.find(prod => prod.id === id); const updatedProducts = products.filter(prod => prod.id !== id); fs.writeFile(p, JSON.stringify(updatedProducts), err => { if (!err) { Cart.deleteProduct(id, product.price); } }); }); }
1
"static" means that we're calling the method on the Class itself rather than an instance of the Class. In "postEditProduct," for example, you see "save" being called on the const, "updatedProduct" which is an instance of the "Product" class...
exports.postEditProduct = (req, res, next) => { const prodId = req.body.productId; const updatedTitle = req.body.title; const updatedimageUrl = req.body.imageUrl; const updatedDesc = req.body.description; const updatedPrice = req.body.price; const updatedProduct = new Product( prodId, updatedTitle, updatedimageUrl, updatedDesc, updatedPrice ); updatedProduct.save(); res.redirect("/admin/products"); };
With the "static" dynamic, we don't have to instantiate the Class, we can call it from the Controller with Product.deleteById(prodId);. Click here for more info.
2
again, we've got a callback. With getProductsFromFile, you're going to be reading from fs.readFile(p, (err, fileContent) and then dropping the results of that file into the "products" variable.
"find" -> will return the first value that matches the specified criteria "filter" -> returns everything that matches the specified criteria "findIndex" -> will grab the index of the first key value pair that matches the specified criteria
3
Whether it's "find" or "filter" or "findIndex," know that this piece: prod=>prod.id===id; ...conceptually is the same as this: for (products as prod) { if (prod.id === id) { //we've got a match return prod; } } return undefined; One thing to keep in mind is the difference between "==" and "===." As a quick aside, you'll never see a single "=" in JavaScript. It'll never happen. But as far as the difference between "==" and "===," it boils down to one word: explicit. "===" is going to look for an equivalent in the context of both the value and the data type. So, if you have "1" as an integer and "1" as a string, "===" will return a value of "false." On the other hand, "1" as an integer compared to "1" as an integer will return "true." "==" doesn't care about datatype. "1" as an integer compared to "1" as a string will return "true." Click here for more information.
4
with "filter," you're creating a new array with all of the elements that satisfy the specified condition. In this case, you're creating an array with all of the products that DON'T have the posted product id.
5
write to the "product.JSON" file the new group of products sans the resource with the id that was posted.
6
if you don't have an error, we're going to now delete that product from the Cart 1) cart.js (back to top...) Here's your "deleteFileById" method in the "cart.js" file:
static deleteProduct(id) { fs.readFile(p, (err, fileContent) => { if (err) { return; } const updatedCart = { ...JSON.parse(fileContent) }; const productIndex = updatedCart.products.find(prod => prod.id === id); const productQty = product.qty; updatedCart.products = updatedCart.products.filter( prod => prod.id !== id ); updatedCart.totalPrice = updatedCart.totalPrice - productPrice * productQty; fs.writeFile(p, JSON.stringify(updatedCart), err => { console.log(err); }); }); } };
1
using the spread operator on the JSON.parse(fileContent). A little different than what we've encountered up to this point, but it works because the "JSON.parse" command is going to convert the "cart.json" file, which by definition is a string, into a JavaScript object which is going to be a collection of key / value pairs. At that point, you'll be able to read it!
2
you're using "find" which is going to find the first element in your array that matches the specified criteria. It differs from "findIndex" in that you're returning an array whereas "findIndex" is simply going to return the index. Click here to learn more. A) Router (back to top...) Your router piece is already in place, but for the sake of review it looks like this: router.get("/cart", shopController.getCart); B) Controller (back to top...) Your Controller is a litte more involved just because you've got two callbacks in quick succession...
exports.getCart = (req, res, next) => { Cart.getCart(cart => { Product.fetchAll(products => { const cartProducts = []; for (product of products) { const cartProductData = cart.products.find( prod => prod.id === product.id ); if (cart.products.find(prod => prod.id === product.id)) { cartProducts.push({ productData: product, qty: cartProductData.qty }); } } res.render("shop/cart", { path: "/cart", pageTitle: "Your Cart", products: cartProducts }); }); }); };
1
you're calling "getCart" from your "cart" model which is going to replace the highlighted cart variable with the parsed result of the "cart.json" file
2
here you're calling the "fetchAll" method from you "products" model which is going to replace the highlighted products variable with the contents of the "products.json" file
3
establish an empty array called "cartProducts." That's going to be the digital container that will hold all of the data that's currently in our cart
4
here's where the magic begins. You're doing a "for" loop where you're indentifying which of the products in the database are actually in your cart. "product" in the "for product piece could be named anything. It just needs to be consistent because "product" is going to be holding all of the key / value pairs that make up that particular product in the database. In other words, "id," "description," "title" can now be retrieved as "product.id," product.description" etc.
5
your "cart.json" file looks like this:
{"products":[{"id":"7747","qty":4},{"id":"0.22422680191225441","qty":2},{"id":"0.9570675800576787","qty":1}],"totalPrice":64.98}
Conseqently, when you call cart.products, remember to make the distinction between the "products" object that's coming from your "products.json" file and the situation here where it's one of the keys in your "cart.json" file. On this line you're locating the product id in the "products.json" file that corresponds to the product id in the "cart.json" file. With that info, you can retrieve the quantity of the product that was ordered.
6
with this "IF" clause, you can grab all of the product info from the "product.json" file based on the fact that the product id in the "cart.json" file matches something in the "product.json" file. If there's a match, you'll "push" all of the product data coming from the "product.json" file that corresponds to the matching product id to the "productData" key value and the quantity ordered will be "pushed" to the "qty" key value.
we don't have to worry about the Model in this case just because we're using functionality that's already been created
A) Router (back to top...) router.post("cart/delete-item", shopControler.postCartDeleteProduct); B) Controller (back to top...)
exports.postCartDeleteProduct = (req, res, next) => { const prodId = req.body.productId; Product.findById(prodId, product => { Cart.deleteProduct(prodId, product.price); res.redirect("/cart"); }); };
1
grab the incoming product id
2
you position "Cart.deleteProduct" as a callback because otherwise JavaScript will start processing that function without first securing the needed information from the "cart.json" file that corresponds to the incoming prodId. That's how you're going to be able to adjust the total price tag after you've made your changes. C) Model Fix (slight bug repair to the "deleteProductd" function) (back to top...) The "deleteProduct" function had one bug in it that needed to be repaired. The changes that were made are highlighted in yellow...
static deleteProduct(id) { fs.readFile(p, (err, fileContent) => { if (err) { return; } const updatedCart = { ...JSON.parse(fileContent) }; const product = updatedCart.products.find(prod => prod.id === id); if(!product){ return; } const productQty = product.qty; updatedCart.products = updatedCart.products.filter( prod => prod.id !== id ); updatedCart.totalPrice = updatedCart.totalPrice - productPrice * productQty; fs.writeFile(p, JSON.stringify(updatedCart), err => { console.log(err); }); }); }
The reason this change was needed is because up to this point we were coding assuming that there was a product in the Cart. That may not always be the case, however. Consequently, you need what's highlighted above in order to avoid an error when the code goes to delete a product that's not in the Cart. A) SQL (back to top...) While this is familiar, let's just go over some basic terms and concepts...B) NoSQL (back to top...) NoSql is different from SQL in ways that are illustrated according to the graphic below:

Scalability...

C) Differences and Advantages (back to top...) The bottom line is and the nature of your queries. 1) ACID (back to top...) SQL Databases are based on the RDBMS system (Relational Database Management System) which is defined by the relational dynamic referred to earlier. One term that you'll hear when talking about the "essence" SQL databases is ACID which stands for Atomicity, Consistency, Isolation and Durability. Here's a great explanation from stackoverflow:
ACID is a set of properties that you would like to apply when modifying a database. Atomicity Consistency Isolation Durability A transaction is a set of related changes which is used to achieve some of the ACID properties. Transactions are tools to achieve the ACID properties. Atomicity means that you can guarantee that all of a transaction happens, or none of it does; you can do complex operations as one single unit, all or nothing, and a crash, power failure, error, or anything else won't allow you to be in a state in which only some of the related changes have happened. Consistency means that you guarantee that your data will be consistent; none of the constraints you have on related data will ever be violated. Isolation means that one transaction cannot read data from another transaction that is not yet completed. If two transactions are executing concurrently, each one will see the world as if they were executing sequentially, and if one needs to read data that is written by another, it will have to wait until the other is finished. Durability means that once a transaction is complete, it is guaranteed that all of the changes have been recorded to a durable medium (such as a hard disk), and the fact that the transaction has been completed is likewise recorded. So, transactions are a mechanism for guaranteeing these properties; they are a way of grouping related actions together such that as a whole, a group of operations can be atomic, produce consistent results, be isolated from other operations, and be durably recorded.
If you could take all of that and boil it down to one sentence it would be "all or nothing." Your query is going to work completely for fail entirely. NoSql tends to be a little more flexible because you've got duplicate data and the absence of relationships that have to be intact in order for you query to succeed. 2) Scalability (back to top...) In addition, you have the issue of scalability. An table in an open source SQL database can handle (roughly) one million rows. An enterprise level SQL server can handle (roughly) fifty million rows. A NoSql database can handle hundreds of millions of rows and the reason for that is scalability. "Scalability" is the process by which your server is expanded to handle more traffic / data. Imagine a truck hauling a trailer. As that trailer gets bigger and heavier, with SQL server, you're going to make your engine bigger. With NoSql, you're going to add another truck. Here's a "graphic" summary of the differences:
A) Installing MySql Package (back to top...) Install the mysql package by entering this on your terminal:
npm install --save mysql2
Now your app can use and understand a MySql paradigm! 1) MySql Workbench Notes (back to top...) Downloading MySql Workbench is pretty intuitive. B) Establishing Database Connection (back to top...) The first thing you're going to do is import the "mysql2" package and store it in a "const." We'll do that by creating a new file called "database.js" and storing it in our "utility" directory. The code will look like this:
const mysql = require("mysql2"); const pool = mysql.createPool({ host: "localhost", user: "root", database: "node-complete", password: "" }); module.exports = pool.promise();
1
here you're importing the "mysql2" pacakge into that application and storing it in the "mysql" const
2
"connection pooling" is a process by which a bunch of connections are made available so that you're not having to initialize and authenticate a new database connection every time one is needed. The default number of connections that are created right out of the box is 10, but you can adjust that with a simple line of code (see image to the right).
3
a "promise" is similar to an IF / ELSE scenario where you're running a callback and proceeding based on the success of that callback. If something goes south, with the "catch" dynamic, you have an error handler by default. Click here for more information. Notice you've got all of your database credentials in place. After all that is documented, you export your database pool with the attached "promised" dynamic and you're set. To make this available in a "global" context, you'll write this in your "app.js" file: const db = require("./utility/database"); BOOM! C) Creating a Table (back to top...) This is a pretty intuitve process, as far as setting up a table in MySql Workbench, but...
...the fields going across the top of the above graphic: PK -> Primary Key NN -> Not Null UQ -> Unique Index (your Id needs to be unique) B -> Is Binary Column UN -> Unsigned Data Type ZF -> fill up that column with zeroes if that column is numeric AI -> Auto Increment G -> Generated Column For your "id" field, you'll need to have: PK NN UQ UN (you don't want any negative integers) AI ...and that will take care of your Primary Key. Everything else as far as "title" etc. is pretty intuitive. D) SELECT Statement (back to top...) Thus far we've imported the "mysql" package into our app with const db = require("./utility/database"); and that, of course, references the "database.js" file in the "utility" directory, we're now ready to start drawing from the "node-complete" database for our product information. The way this is going to look is this: db.execute('SELECT * FROM products') .then(result => { console.log(result[0], result[1]); }) .catch(err => { console.log(err); }); The results of your SELECT are coming back to you in the context of a Promise with two nested arrays contained within your "next" element. Those two arrays look like this:
[ BinaryRow { id: 1, title: 'Muscular Christianity', price: 10.99, description: 'A great book!', imageUrl: 'http://muscularchristianityonline.com/forum/wp-content/uploads/2017/04/book_display.jpg' } ] [ { catalog: 'def', schema: 'node-complete', name: 'id', orgName: 'id', table: 'products', orgTable: 'products', characterSet: 63, columnLength: 10, columnType: 3, flags: 16935, decimals: 0 }, { catalog: 'def', schema: 'node-complete', name: 'title', orgName: 'title', table: 'products', orgTable: 'products', characterSet: 224, columnLength: 1020, columnType: 253, flags: 4097, decimals: 0 }, { catalog: 'def', schema: 'node-complete', name: 'price', orgName: 'price', table: 'products', orgTable: 'products', characterSet: 63, columnLength: 22, columnType: 5, fl flags: 4097, decimals: 31 }, { catalog: 'def', schema: 'node-complete', name: 'description', orgName: 'description', table: 'products', orgTable: 'products', characterSet: 224, columnLength: 262140, columnType: 252, flags: 4113, decimals: 0 }, { catalog: 'def', schema: 'node-complete', name: 'imageUrl', orgName: 'imageUrl', table: 'products', orgTable: 'products', characterSet: 224, columnLength: 1020, columnType: 253, flags: 4097, decimals: 0 } ]
The first array is pretty obvious, as far as it being the information about the products in the table. The second array is information about the table itself and you can learn more about that by clicking here. We're going to structure our SELECT statement in a way that incorporates the "promise" dynamic we discussed earlier. This is an "IF / ELSE" scenario that handles a callback in a way that's both elegant and easy to understand and provides a "catch" scenario so we've got the ability to handle errors by default. The "result" comes back as an array which we can display in the console in a way that's a little more organized than what it would be otherwise by specifying the index. The second array isn't necessary, but it's good to know for the sake of theory. Here's the way it's going to show up in practice... 1) Model (back to top...) static fetchAll() { return db.execute("SELECT * FROM products"); } Pretty straight forward... Now, here's our Controller: 2) Controller (back to top...) exports.getIndex = (req, res, next) => { Product.fetchAll() .then(([rows]) => { res.render("shop/index", { prods: rows, pageTitle: "Shop", path: "/" }); }) .catch(err => console.log(err)); }; BOOM! E) INSERT Statement 1) Model (back to top...) It's pretty impressive on how the actual code that you're going to use is a lot less cumbersome. Here's our Controller code: save() { return db.execute( "INSERT INTO products (title, price, description, imageUrl) VALUES (?, ?, ?, ?)", [this.title, this.price, this.description, this.imageUrl] ); } We're using "?" so as to avoid any chance of SQL Injection. Everything else is pretty streamlined. Don't forget to "return" the result of your executed statement .You'll need that for your "then" object (see below). Otherwise, you'll an error message that says, "then" is not defined. 2) Controller (back to top...) Very similiar to what we had before, we're just introducing our Promise with the "save" function. exports.postAddProduct = (req, res, next) => { const title = req.body.title; const description = req.body.description; const imageUrl = req.body.imageUrl; const price = req.body.price; const product = new Product(null, title, imageUrl, description, price); product .save() .then(() => { res.redirect("/"); }) .catch(err => console.log(err)); }; Do you smell that? That's the aroma of excellence! E) View Details (back to top...) Here's how you're going to view the details of a particular product using SQL... 1) Model Here's your Model: static findById(id) { return db.execute("SELECT * FROM products WHERE products.id = ?", [id]); } 2) Controller Here's your Controller: exports.getProduct = (req, res, next) => { const prodId = req.params.productId; Product.findById(prodId) .then(([product]) => { res.render("shop/product-detail", { product: product[0], // here's how you limit what's returned to the first array as opposed to the "FieldData" that's also going to be included pageTitle: "product.title", path: "/products" }); }) .catch(err => console.log(err)); }; A) Definition (back to top...) Basically, "Sequelize" is a third party package that allows you to write and retrieve SQL statements as JavaScript objects. See graphic below:
The basic idea behind Sequelize looks like this:
B) Connecting to Database (back to top...) Start by installing the Sequelize package:
npm install --save sequelize
BTW: MySql needs to be installed. In this case, we've already done that, but note to self! Sequelize is going to do a lot of what we did with our initial MySql connection setup, but it will do it automatically. For example, it will set up the connection pool. Again, it's less code, yet you're writing more syntax. Here's the code for the connection: const Sequelize = require('sequelize'); const sequelize = new Sequelize('node-complete', 'root', '', { dialect: 'mysql', host: 'localhost' }); module.exports = sequelize; C) Defining a Model (back to top...) We're in our "product.js" model file and we're going to write this:
const Sequelize = require('sequelize');// import Sequelize itself const sequelize = require('../utility/database'); // establish our connection const Product = sequelize.define('product', { id: { type: Sequelize.INTEGER, autoIncrement: true, allowNull: false, primaryKey: true }, title: Sequelize.STRING, price: { type:Sequelize.DOUBLE, allowNull: false }, imageUrl: { type: Sequelize.STRING, allowNull: false }, description: { type: Sequelize.STRING, allowNull: false } }); module.exports = Product;
For more info about Sequelize datatypes, click here. Once you've got that table / Model defined, you write this in your app.js file: const sequelize = require("./utility/database"); sequelize .sync() // this command here is going to build your table based on the model you defined in your "product.js" file .then(result => { console.listen(3000); }) .catch(err => { console.log(err); }); When you run start up your app, you'll see in your console based on the above code:
Executing (default): CREATE TABLE IF NOT EXISTS `products` (`id` INTEGER NOT NULL , `title` VARCHAR(255), `price` DOUBLE PRECISION NOT NULL, `imageUrl` VARCHAR(255) NOT NULL, `description` VARCHAR(255) NOT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Notice the additional columns: "createdAt" and "updatedAt" that we get by default. Pretty cool... D) Creating and Inserting a Product (back to top...) On our "admin.js" file, here's the "before" and "after" code for adding a product given the fact that we're not using Sequelize as opposed to straight MySql.
products.js (MySql)
  1. exports.postAddProduct = (req, res, next) => {
  2. const title = req.body.title;
  3. const description = req.body.description;
  4. const imageUrl = req.body.imageUrl;
  5. const price = req.body.price;
  6. const product = new Product(null, title, imageUrl, description, price);
  7. product
  8. .save()
  9. .then(() => {
  10. res.redirect("/");
  11. })
  12. .catch(err => console.log(err));
  13. };
products.js (model [storing data in products.json])
  1. exports.postAddProduct = (req, res, next) => {
  2. const title = req.body.title;
  3. const description = req.body.description;
  4. const imageUrl = req.body.imageUrl;
  5. const price = req.body.price;
  6. Product.create({
  7. title: title,
  8. price: price,
  9. imageUrl: imageUrl,
  10. description: description
  11. })
  12. .then(result => {
  13. //console.log(result);
  14. console.log("Created Product!");
  15. })
  16. .catch(err => {
  17. console.log(err);
  18. });
  19. };
D) Retrieving Products (back to top...) We're just going to use the Sequelize "findAll" function that comes with Sequelize right out of the box. It will look like this: exports.getProducts = (req, res, next) => { Product.findAll() .then(products => { // you can name "products" anything you want res.render("shop/index", { prods: products, pageTitle: "Shop", path: "/" }); }) .catch(err => { console.log(err); }); }; E) Retrieving One Product (back to top...) 1) findByPk (back to top...) There are two ways to retrieve an individual product. One is with "findByPk..." exports.getProduct = (req, res, next) => { const prodId = req.params.productId; Product.findByPk(prodId) .then(product => { // instead of .then(([product]) res.render("shop/product-detail", { product: product, // instead of product[0] pageTitle: "product.title", path: "/products" }); }) .catch(err => console.log(err)); }; It's almost the same code that we were using before with the exception of "findByPk" and also we're not getting any nested arrays back as a result, just one array. 2) where: { id: prodId } (back to top...) exports.getProduct = (req, res, next) => { const prodId = req.params.productId; Product.findAll({ where: { id: prodId } }) .then(products => { // indexed array res.render("shop/product-detail", { product: products[0], // indexed array pageTitle: "products[0].title", path: "/products" }); }) You're getting an indexed array so you have to document / retrieve it as such. F) Editing a Product (back to top...) Here's how you're going to edit a product using Sequelize:
exports.postEditProduct = (req, res, next) => { const prodId = req.body.productId; const updatedTitle = req.body.title; const updatedImageUrl = req.body.imageUrl; const updatedDesc = req.body.description; const updatedPrice = req.body.price; Product.findByPk(prodId) // find your product .then(product => { // here's your first "promise" product.title = updatedTitle; product.price = updatedPrice; product.description = updatedDesc; product.price = updatedPrice; product.imageUrl = updatedImageUrl; return product.save(); // rather than attach another "then" to what is, by default a promise because it's a callback, we're going to "return" the result of a successful "then" }) .then(result => { // this "then" covers both the "findByPk" and the "product.save" console.log("Updated Product!"); res.redirect("/admin/products"); // put your redirect here as a part of your "then" dynamic so it reflects the changes made }) .catch(err => { console.log(err); }); };
G) Deleting a Product (back to top...) Here's how you delete a product:
exports.postDeleteProduct = (req, res, next) => { const prodId = req.body.productId; Product.findByPk(prodId) .then(product => { return product.destroy(); }) .then(result => { console.log("product is deleted"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); };
Pretty straight forward! H) Adding a One to Many Relationship (back to top...) We've set up a User model and that's going to be created or at least confirmed when we first hit the app.js file. To establish relationships (or Associations as its called in Sequelize) like what you've got in SQL, you do it like this:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const app = express(); app.set("view engine", "ejs"); app.set("views", "views"); const adminData = require("./routes/admin"); const shopRoutes = require("./routes/shop"); const errorController = require("./controllers/error"); const sequelize = require("./utility/database"); const Product = require("./models/product"); const User = require("./models/user"); app.use(bodyParser.urlencoded()); app.use(express.static(path.join(__dirname, "public"))); app.use("/admin", adminData.routes); app.use(shopRoutes); app.use(errorController.get404); Product.belongsTo(User, { constraints: true, onDelete: "CASCADE" }); User.hasMany(Product); sequelize .sync({ force: true }) .then(result => { //console.listen(3000); }) .catch(err => { console.log(err); }); app.listen(3000);
1
simply us referening and the "product" and the "user" models
2
here we're defining the relationships between the "Product" and the "User" tables. We're stating that the user creates the products in the products table.
3
with this, we're "forcing" the syncronization of the two tables. This way, if a user is deleted, all of the products associated with that user will be deleted as well. What's great about this approach is that when the app fires up, the "products" table, which would not be touched if there were already products documented, but because we've got the sync({ force: true }) in place, that table will be made from scratch AND will include a new "user_id" field. H) Adding a Dummy User (back to top...) To add a "dummy" user, you're going to add this to your "app.js" file: sequelize //.sync({ force: true }) .sync() .then(result => { return User.findByPk(1);// looking to see if there's anything in the user table }) .then(user => { if (!user) { "user" is coming from your User model where the "user" variable is defined User.create({ name: "Bruce", email: "bruce@brucegust.com" }); // new user is created if nothing exists } return user; // you're going to return your result instead of introducing another promise }) .then(user => { // here's your last "promise." If everything is either added or recognized, then we'll log it in our console console.log(user); app.listen(3000); }) .catch(err => { console.log(err); }); 1) Establishing User as Part of the Request Object (back to top...) By making our user a part of the "request" object, we've got access to a Sequelize object with all of the corresponding information throughout our app. It's kind of like a convenient session variable. Here's how you do it and to refresh your memory about "Middleware," click here app.use((req, res, next) => { User.findByPk(1) // because this is middleware, it will run after your "sequelize" command finishes and establishe the database connection etc .then(user => { req.user = user; // here is where you're associating your "user" from the above "findByPk" function with the "req" (request) object next(); // here you include the "next" element so your code is allowed to continue }) .catch(err => { console.log(err); }); }); I) Using Magic Methods To add a product, you need to change things a little bit just because you now have the extra "user_id" column. To do that, you would simply code this: exports.postAddProduct = (req, res, next) => { const title = req.body.title; const description = req.body.description; const imageUrl = req.body.imageUrl; const price = req.body.price; Product.create({ title: title, price: price, imageUrl: imageUrl, description: description, userId: req.user.id }) .then(result => { //console.log(result); console.log("Created Product!"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); }; That's it! But with Sequelize, you have a more streamlined and elegant way to pull this off just because of the way the Associations within Sequelize are set up. It looks like this: exports.getEditProduct = (req, res, next) => { const editMode = req.query.edit; if (!editMode) { return res.redirect("/"); } const prodId = req.params.productId; req.user // grab user thing .getProducts({ where: { id: prodId } }) // here's your new code and then you just string your "then's" afterwards like you did before .then(products => { const product = products[0]; // you need to grab the first indexed array if (!product) { return res.redirect("/"); } res.render("admin/edit-product", { pageTitle: "Edit Product", path: "/admin/edit-product", editing: editMode, product: product }); }) .catch(err => { console.log(err); }); }; We're going to change our "getProducts" on the "admin.js" page a little bit by limiting the displayed products to the ones that correspond to the user id... exports.getProducts = (req, res, next) => { req.user .getProducts() .then(products => { res.render("admin/products", { prods: products, pageTitle: "Admin Products", path: "/admin/products" }); }) .catch(err => { console.log(err); }); }; J) One to Many & Many to Many Relationships (back to top...) We're going to start from scratch, as far as the "cart.js" file. Here's the original code:
const fs = require("fs"); const path = require("path"); const p = path.join( path.dirname(process.mainModule.filename), "data", "cart.json" ); module.exports = class Cart { static addProduct(id, productPrice) { //fetch the previous cart fs.readFile(p, (err, fileContent) => { let cart = { products: [], totalPrice: 0 }; if (!err) { cart = JSON.parse(fileContent); } //analyze the cart => find existing product const existingProductIndex = cart.products.findIndex( prod => prod.id === id ); const existingProduct = cart.products[existingProductIndex]; let updatedProduct; //add new product / increase quantity if (existingProduct) { updatedProduct = { ...existingProduct }; updatedProduct.qty = updatedProduct.qty + 1; cart.products = [...cart.products]; cart.products[existingProductIndex] = updatedProduct; } else { updatedProduct = { id: id, qty: 1 }; cart.products = [...cart.products, updatedProduct]; } cart.totalPrice = cart.totalPrice + +productPrice; fs.writeFile(p, JSON.stringify(cart), err => { console.log(err); }); }); } static deleteProduct(id) { fs.readFile(p, (err, fileContent) => { if (err) { return; } const updatedCart = { ...JSON.parse(fileContent) }; const product = updatedCart.products.find(prod => prod.id === id); if (!product) { return; } const productQty = product.qty; updatedCart.products = updatedCart.products.filter( prod => prod.id !== id ); updatedCart.totalPrice = updatedCart.totalPrice - product.price * productQty; fs.writeFile(p, JSON.stringify(updatedCart), err => { console.log(err); }); }); } static getCart(cb) { fs.readFile(p, (err, fileContent) => { const cart = JSON.parse(fileContent); if (err) { cb(null); } else { cb(cart); } }); } };
Here's what we have now:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const app = express(); app.set("view engine", "ejs"); app.set("views", "views"); const adminData = require("./routes/admin"); const shopRoutes = require("./routes/shop"); const errorController = require("./controllers/error"); const sequelize = require("./utility/database"); const Product = require("./models/product"); const User = require("./models/user"); const Cart = require("./models/cart"); const CartItem = require("./models/cart-item"); app.use(bodyParser.urlencoded()); app.use(express.static(path.join(__dirname, "public"))); app.use((req, res, next) => { User.findByPk(1) .then(user => { req.user = user; next(); }) .catch(err => { console.log(err); }); }); app.use("/admin", adminData.routes); app.use(shopRoutes); app.use(errorController.get404); Product.belongsTo(User, { constraints: true, onDelete: "CASCADE" }); User.hasMany(Product); User.hasOne(Cart); Cart.belongsTo(User); Cart.belongsToMany(Product, { through: CartItem }); Product.belongsToMany(Cart, { through: CartItem }); sequelize .sync({ force: true }) //.sync() .then(result => { return User.findByPk(1); //console.listen(3000); }) .then(user => { if (!user) { User.create({ name: "Bruce", email: "bruce@brucegust.com" }); } return user; }) .then(user => { console.log(user); app.listen(3000); }) .catch(err => { console.log(err); });
A single product can show up in multiple carts and a single cart can have multiple products. To link these two tables together, you establish a "through" dynamic that identifies the model that holds data from both tables and, in so doing, links them together. In this instance, that table is the "cartitems" table which is the "CartItem" model.
What you're doing her is adding a couple new tables and defining the relationship between them.
1
import the new models as they're documented in the "models" directory
2
A single product can show up in multiple carts and a single cart can have multiple products. To link these two tables together, you establish a "through" dynamic that identifies the model that holds data from both tables and, in so doing, links them together. In this instance, that table is the "cartitems" table which is the "CartItem" model. K) Creating and Fetching a Cart (back to top...) 1) Create Cart (back to top...)
sequelize //.sync({ force: true }) .sync() .then(result => { return User.findByPk(1); //console.listen(3000); }) .then(user => { if (!user) { return User.create({ name: "Bruce", email: "bruce@brucegust.com" }); } return user; }) .then(user => { //console.log(user); return user.createCart(); }) .then(cart => { app.listen(3000); }) .catch(err => { console.log(err); })
With the highlighed code, we're creating an empty, but neverthless present "cart" for the user. If you don't have some kind of cart, your code will return an "undefined" when you go to the Cart page. You don't want that... Now that you've got your "cart" model, here's how you would retrieve the contents of that cart. 2) Retrieve Cart (back to top...)
exports.getCart = (req, res, next) => { // this first "getCart" piece of the code is what your router is looking for. It's not a "sequelize" syntax req.user.getCart().then(cart => { return cart .getProducts() .then(products => { res.render("shop/cart", { path: "/cart", pageTitle: "Your Cart", products: products }); }) .catch(err => console.log(err)); });
1
"getCart" is understood by Sequelize as a special kind of method that's in place because of the associations we put in place that exist between the "user" and the "cart" tables.
2
this is one of the "magic methods" that Sequelize provides based on the relationship that exists between the "cart" and the "products" table... Cart.belongsToMany(Product, { through: CartItem }); One thing to keep in mind is that the above code is actually cart.getProducts().then... Click here for more information. BTW: The "magic" is happening when you code things like cart.getProducts(). Otherwise, you're going to be referencing the "getProducts" method that was originally written earlier (you can see that by recognizing the fact that the other "getProducts" is rendering the "Shop" page). L) Adding Products to the Cart (back to top...) 1) Adding a Brand New Product (back to top...) To add a brand new product to the cart, we're going to change things to the Sequelize dynamic and that's going to look like this: shop.js...
exports.postCart = (req, res, next) => { const prodId = req.body.productId; let fetchedCart; req.user .getCart() .then(cart => { fetchedCart = cart; return cart.getProducts({ where: { id: prodId } }); }) .then(products => { let product; if (products.length > 0) { product = products[0]; } let newQuantity = 1; if (product) { //... } return Product.findByPk(prodId) .then(product => { return fetchedCart.addProduct(product, { through: { quantity: newQuantity } }); }) .catch(err => console.log(err)); }) .then(() => { res.redirect('/cart'); }); };
1
we're going to set up a variable that will hold the current cart information. What follows is what we were looking at in the previous example.
2
provided we have a "cart" based on "req.user.getCart," we'll store the results of that query in "fetchedCart."
3
Again, the reason this works is because of what you have in your "app.js" file, specifically the way you have your tables / relationships set up between the "cart" and the "products" table: Cart.belongsToMany(Product, { through: CartItem }); ...as a result, return cart.getProducts({ where: { id: prodId } }); is processed as a SELECT with an INNER JOIN on "prodId." A couple of important things about tables / queries: When you set up your model for a table, regardless of how you define your table, it will automaticaly be named according to the plural version of that model name... // disable the modification of table names; By default, sequelize will automatically // transform all passed model names (first parameter of define) into plural. // if you don't want that, set the following freezeTableName: true, Click here to see that in the Sequelize documentation. What we're doing with this particular line of code we're looking to see if the product the user has selected is already in the cart.
4
if our cart.getProducts query is successful, then were going to set up a "product" variable" and store the results of the query in that variable.
5
put the first part of the products array into the "product" variable
6
grabbing the product id of the product that has been selected by the shopper.
7
provided we get a result from the previous SELECT (findByPK), we'll prime the pump for what the next line of code where we'll enter in the new product into the database
8
another Magic Method that allows us to insert into the appropriate table
9
last part of the INSERT statement that establishes the last field which is "quantity." The "cart" table will be populated with the user_id and the product id, but this last step is what populates the "quantity" field. cart.ejs...
<%- include('../includes/head.ejs') %> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <% if(products.length >0) { %> <ul> <% products.forEach(p => { %> <li> <p></p><%=p.productData.title %> (<%=p.qty%>)</p> <form action="/cart-delete-item" method="Post"> <input type="hidden" name="productId" value="<%=p.productData.id %>"> <button class="btn" type="submit">Delete</button> </form> </li> <% }) %> </ul> <% } else { %> <h1>No Products in Cart!</h1> <% } %> </main> <%- include('../includes/end.ejs') %>
You don't need any "productData" anymore. That's the array the previous round of code was having to drill down into to get the right value. 1) Adding an Existing Product (back to top...)
exports.postCart = (req, res, next) => { const prodId = req.body.productId; let fetchedCart; let newQuantity = 1; req.user .getCart() .then(cart => { fetchedCart = cart; return cart.getProducts({ where: { id: prodId } }); // right here is where we're doing a SELECT between the "cart" and the "products" table. And remember the relationship or the, "through" dynamic which means that the table "cartitems" is included by default. }) .then(stuff => { let product; if (stuff.length > 0) { product = stuff[0]; } if (product) { const oldQuantity = product.cartItem.quantity; newQuantity = oldQuantity+1; return product; } return Product.findByPk(prodId); }) .then(product => { return fetchedCart.addProduct(product, { through: { quantity: newQuantity } }); }) .then(() => { res.redirect("/cart"); }); };
1
you can see the SELECT that's being triggered by going to the console. Here's what it looks like: Executing (default): SELECT `id`, `quantity`, `createdAt`, `updatedAt`, `cartId`, `productId` FROM `cartItems` AS `cartItem` WHERE `cartItem`.`cartId` = 1 AND `cartIte`.`productId` IN (1);
2
the "then" block based on the successful completion of the previous function. In this case, it's the successful retrieval of the cart that's associated with this user.
3
if we've got a cart for this user, then we're going to store the first indexed array of the returned result in a variable called "product." This is going to be the actual product info you've got in the cart that matched the product id of the resource the user clicked on.
4
if you've got a match, you're going to store the current "quantity" in the database that corresponds to this product id in a const called, "oldQuantity" and that's going to be accessed by "product.cartItem.quantity." You're going to add "1" to that value and then "return." That's the end of what amounts to an "IF" clause.
ORM stands for Object Relational Mapping and refers to the relationships between tables.
5
if the previous block of code didn't get "returned," then you're at this point where you're going to return the product information that corresponds to the posted product id.
6
you're either adding new product info or you're updating what's currently in the database. And it does that all by itself! Here's the code that's being run based on the IF statement that qualifies things as being either a new product or simply updating what's already there: Executing (default): UPDATE `cartItems` SET `quantity`=?,`updatedAt`=? WHERE `cartId` = ? AND `productId` = ? And this is all a result of Magic Methods! M) Deleting Products From the Cart (back to top...) To delete a product from the Cart, it's pretty straight forward...
exports.postCartDeleteProduct = (req, res, next) => { const prodId = req.body.productId; req.user .getCart() .then(cart => { return cart.getProducts({ where: { id: prodId } }); }) .then(products => { const product = products[0]; return product.cartItem.destroy(); }) .then(result => { res.redirect("/cart"); }) .catch(err => console.log(err)); };
1
you're getting this from the hidden field "productId"
2
getting your cart using Magic Method that's based on your userId
3
retrieving the products from the product table that coincide with the posted product id
4
provided you've got a successful query, we're grabbing the first part of the returned "products" array. Once we've got access to that piece, we're "destroying" the element that goes along with the "cartItem" dynamic which is going to be your productId N) Adding an Order (back to top...) We've got a Cart, now let's set things up in a way where we can view and delete orders. 1) Setting up Model (back to top...) First thing you're going to do is set up your Model and the appropriate relationships. You'll need "order.js..."
const Sequelize = require("sequelize"); const sequelize = require("../utility/database"); const Order = sequelize.define("order", { id: { type: Sequelize.INTEGER, autoIncrement: true, allowNull: false, primaryKey: true } }); module.exports = Order;
...and "order-item.js..."
const Sequelize = require("sequelize"); const sequelize = require("../utility/database"); const OrderItem = sequelize.define("orderItem", { id: { type: Sequelize.INTEGER, autoIncrement: true, allowNull: false, primaryKey: true }, quantity: Sequelize.INTEGER }); module.exports = OrderItem;
Absolutely no big deal. Now, let's look at our "app.js" file and consider the relationships: First, you'll import the actual model: const Order = require("./models/order"); const OrderItem = require("./models/order-item"); ...and then you'll define their relationships. Order.belongsTo(User); // each order has only one user User.hasMany(Order); // a user can conceivably have many orders Order.belongsToMany(Product, { through: OrderItem }); // one order can have many products and it's the "order-item" table that has the column headings that provide the "link" between those two tables 1) Setting up Router and Controller (back to top...) Here's your router: router.post("/create-order", shopController.postOrder); Your Controller looks like this:
exports.postOrder = (req, res, next) => { req.user .getCart() .then(cart => { return cart.getProducts(); }) .then(products => { return req.user .createOrder() .then(order => { return order.addProducts( products.map(product => { product.orderItem = { quantity: product.cartItem.quantity }; return product; }) ); }) .catch(err => console.log(err)); }) .then(result => { res.redirect("/orders"); }) .catch(err => console.log(err)); };
1
get all of the products in the current cart that correspond to the current user
2
upon successful completion of the "cart / products " query, we now move all of the items in the cart to the "order" and "order-items" table.
3
because of the relationship that exists between the order table and the user, we can create a new order by referencing the user id.
4
when you do a console.log(product), you get this:
id: 1, title: '90 Day Tour of the Bible', price: 4.5, imageUrl: 'https://images-na.ssl-images-amazon.com/images/I/51i7RDuPgGL._SX322_BO1,204,203,200_.jpg', description: ' Bible Study ', createdAt: 2019-05-02T19:55:55.000Z, updatedAt: 2019-05-02T19:55:55.000Z, userId: 1, cartItem: [cartItem] },
Notice that the "quantity" field then we're needing in the "cart-item" field is referenced as "cart-item" and not the actual quantity value. Because of that we need to do a little calculation for every value in our array. For that, we'll use map - a function we have available to us in JavaScript.
5
now that we've got the cartItem value, we can plug it in as the "quantity" value Once that's in place, while it won't show up on our "order" page, it will show up in our console as a completed order. Yay! O) Resetting the Cart and Outputting Orders (back to top...) 1) Reset the Cart (back to top...) After you post the order from the "cart" to the "order" table, you want to reset the Cart so it's empty. You do that by using this code: .then(result => { return fetchedCart.setProducts(null); }) This will clean out the "cart-item" table. You'll still have your user in the "cart" table. 2) Router Error (back to top...) At one point, I got this error:
C:\wamp\www\adm\node\express_tutorial\node_modules\express\lib\router\route.js:202 throw new Error(msg); ^ Error: Route.get() requires a callback function but got a [object Undefined]
It was referring to a route that didn't have a corresponding Controller. I had deleted a superflous route in the Controller, but left the same route in the router. That will throw an error! Once I delete the route in the route.js file, it was all good! 3) Outputting Orders (back to top...)
exports.getOrders = (req, res, next) => { req.user .getOrders({ include: ['products'] }) .then(orders => { console.log(orders.products); res.render("shop/orders", { path: "/orders", pageTitle: "Your Orders", orders: orders }); }) .catch(err => console.log(err)); };
When you output products, based on the Magic Methods available through Sequelize, you get this:
[ order { dataValues: { id: 1, createdAt: 2019-05-03T15:00:30.000Z, updatedAt: 2019-05-03T15:00:30.000Z, userId: 1, products: [Array] }, _previousDataValues: { id: 1, createdAt: 2019-05-03T15:00:30.000Z, updatedAt: 2019-05-03T15:00:30.000Z, userId: 1, products: [Array] }, _changed: {}, _modelOptions: { timestamps: true, validate: {}, freezeTableName: false, underscored: false, paranoid: false, rejectOnEmpty: false, whereCollection: [Object], schema: null, schemaDelimiter: '', defaultScope: {}, scopes: {}, indexes: [], name: [Object], omitNull: false, sequelize: [Sequelize], hooks: {} }, _options: { isNewRecord: false, _schema: null, _schemaDelimiter: '', include: [Array], includeNames: [Array], includeMap: [Object], includeValidated: true, attributes: [Array], raw: true }, isNewRecord: false, products: [ [product] ] } ]
Because of the relationship between the "products" table and the "orders" table: Order.belongsToMany(Product, { through: OrderItem }); Your product info is going to be represented by a multi-dimensional array and you're not going to be able to access it without something called, "Eager Loading." It sounds technical, but it's just a term used to describe a comprehensive approach to loading data. Here's a good explanation:
Sometimes you have two entities and there's a relationship between them. For example, you might have an entity called University and another entity called Student. The University entity might have some basic properties such as id, name, address, etc. as well as a property called students: public class University { private String id; private String name; private String address; private List<Student> students; // setters and getters } Now when you load a University from the database, JPA loads its id, name, and address fields for you. But you have two options for students: to load it together with the rest of the fields (i.e. eagerly) or to load it on-demand (i.e. lazily) when you call the university's getStudents() method. When a university has many students it is not efficient to load all of its students with it when they are not needed. So in suchlike cases, you can declare that you want students to be loaded when they are actually needed. This is called lazy loading.
In this case, "eager loading" is going to be looping through every "products" array that occurs. And that's what you have with "orders.ejs:"
<main> <% if(orders.length <= 0) { %> <h1>Nothing there!</h1> <% } else { %> <ul> <% orders.forEach(order => { %> <li> <h1># <%= order.id %></h1> <ul> <% order.products.forEach(product => { %> <li><%= product.title %> (<%= product.orderItem.quantity %>)</li> <% }); %> </ul> </li> <% }); %> </ul> <% } %> </main>
And that's it! A) Basics (back to top...) 1) BSON (back to top...) Here's the way data is stored in Mongo (think JSON):
BSON stands for "Binary JSON." It can hold embedded documents (multi-dimensional arrays), objects and all kinds of things... 2) Relationships (back to top...) What makes Mongo quicker is that you're only retrieving the data you need. Because of it being "schema-less," you're not having to grab all of the information that might otherwise be stored in a table. Also, your relationships are less involved because you're not obligated to create JOINS every time you're needing some info because you could very well have all the info you need in a particular document without having to go all over the database to get what you need.

Mongo Callback
3) Installation (back to top...) You'll go out to the Mongo website andOnce that's in place, you write npm install --save mongo db to install the MongoDB driver. 4) Connection (back to top...) You've got your package and your driver installed, now you need to set up your connection and you'll use your "database.js" file to do that. Here's how that file looks:
const mongodb = require("mongodb"); const MongoClient = mongodb.MongoClient; const mongoConnect = callback => { MongoClient.connect( "mongodb+srv://brucegust:Mu5cular!@brucegust-qhxnz.mongodb.net/test?retryWrites=true" ) .then(client => { console.log("Connected"); callback(client); }) .catch(err => { console.log(err); }); }; module.exports = mongoConnect;
1
import the MongoDB package
2
extract the MongoClient constructor from the MongoDB package
3
using an arrow function and a callback to retrive the database connection
4
this is the connection info that you'll copy from the Mongo website when you click on the "connect" button in the "cluster" tab (see image to the right)
Be aware that your "IP Whitelist" is crucial in that if you're working on your site from various locations, your IP address will change and that will affect your connectivity. Also, avoid special characters in your password as those will be processed as HTML entities.
i) Connection Pool (back to top...) We're going to alter things a bit in order to accommodate something called the "Connection Pool." You've seen this before with MySql and Sequelize. Here's the altered "database.js" code:
const mongodb = require("mongodb"); const MongoClient = mongodb.MongoClient; let _db; const mongoConnect = callback => { MongoClient.connect( "mongodb+srv://brucegust:Mu5cular!@brucegust-qhxnz.mongodb.net/shop?retryWrites=true" ) .then(client => { console.log("Connected"); _db = client.db() callback(); }) .catch(err => { console.log(err); throw.err; }); }; const getDb = () => { if (_db) { return _db; } throw 'No database found'; }; module.exports = mongoConnect; exports.getDb = getDb; //this is from your app.js file... mongoConnect(() => { app.listen(3000); });
1
establish a new variable called "_db"
2
within the MongoClient string, you've got a "db" object which is the name of the database itself. In this case, it's "shop. You can read more about this element by clicking here.
3
you're invoking your callback just like you did before. But you're not passing an argument this time.

Error Messages
4
"throw" is a way in which you customize your errors
5
ES6 Arrow Function without any argument
6
"throw" is something JavaScript offers as a way in which you can customize errors. Click on the "Captain's Log" graphic to the right to see more information about errors as far as how to customize them and how to interpret them when generated by the system.
7
whereas before you had only one method with which you could connect to the database, now you have two. The first one connects to the database, the second one stores that connection. B) Using Database Connection (back to top...) Now that we've got a database connection happening, let's use it! Let's start with the "products.js" Model... 1) products.js (back to top...)
const getDb = require("../utility/database").getDb; class Product { constructor(title, price, description, imageUrl) { this.title = title; this.price = price; this.description = description; this.imageUrl = imageUrl; } save() { const db = getDb(); //grab your connection return db .collection("products") //choose / establish your collection .insertOne(this) .then(result => { console.log(result); }) .catch(err => { console.log(err); }); } } module.exports = Product;
Pretty intuitive at this point. Now, here's your Controller 2) admin.js (back to top...)
const Product = require("../models/product"); exports.getAddProduct = (req, res, next) => { console.log("here"); res.render("admin/edit-product", { pageTitle: "Add Product", path: "/admin/add-product", editing: false }); }; exports.postAddProduct = (req, res, next) => { const title = req.body.title; const price = req.body.price; const description = req.body.description; const imageUrl = req.body.imageUrl; const product = new Product(title, price, description, imageUrl); product .save() .then(result => { console.log("Created Product!"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); };
1
The first part of your "Product" class in your Model looks like this: class Product { constructor(title, price, description, imageUrl) { this.title = title; this.price = price; this.description = description; this.imageUrl = imageUrl; } We're "feeding" those entities with the first part of our code here in our Controller.
CRUD stands for "Create, Read, Update and Delete To read all of the documentation / options where interacting with a Mongo Database is concerned, click here.
2
Here's your "save" functionality on your "products.js" Model: save() { const db = getDb(); //grab your connection return db .collection("products") //choose / establish your collection .insertOne(this) // grabbing the data coming in via the constrcutors .then(result => { console.log(result); }) .catch(err => { console.log(err); }); } } Pretty straight forward...! Right now, you can't see the new product in our application, however, you can see evidence of it having been added in our console...
CommandResult { result: { n: 1, opTime: { ts: [Timestamp], t: 1 }, electionId: 7fffffff0000000000000001, ok: 1, operationTime: Timestamp { _bsontype: 'Timestamp', low_: 2, high_: 1557235827 }, '$clusterTime': { clusterTime: [Timestamp], signature: [Object] } }, connection: Connection { _events: { error: [Function], close: [Function], timeout: [Function], parseError: [Function], message: [Function] }, _eventsCount: 5, _maxListeners: undefined, id: 1, options: { host: 'brucegust-shard-00-00-qhxnz.mongodb.net', port: 27017, size: 5, minSize: 0, connectionTimeout: 30000, socketTimeout: 360000, keepAlive: true, keepAliveInitialDelay: 300000, noDelay: true, ssl: true, checkServerIdentity: true, ca: null, crl: null, cert: null, key: null, passPhrase: null, rejectUnauthorized: false, promoteLongs: true, promoteValues: true, promoteBuffers: false, reconnect: false, reconnectInterval: 1000, reconnectTries: 30, domainsEnabled: false, disconnectHandler: [Store], cursorFactory: [Function], emitError: true, monitorCommands: false, socketOptions: {}, setName: 'brucegust-shard-0', promiseLibrary: [Function: Promise], clientInfo: [Object], user: 'brucegust', password: 'M1ch3ll3', read_preference_tags: null, retryWrites: true, authSource: 'admin', readPreference: [ReadPreference], rs_name: 'brucegust-shard-0', dbName: 'test', servers: [Array], auth: [Object], server_options: [Object], db_options: [Object], rs_options: [Object], mongos_options: [Object], socketTimeoutMS: 360000, connectTimeoutMS: 30000, credentials: [MongoCredentials], monitoring: false, parent: [ReplSet], bson: BSON {} }, logger: Logger { className: 'Connection' }, bson: BSON {}, tag: undefined, maxBsonMessageSize: 67108864, port: 27017, host: 'brucegust-shard-00-00-qhxnz.mongodb.net', socketTimeout: 360000, keepAlive: true, keepAliveInitialDelay: 300000, connectionTimeout: 30000, responseOptions: { promoteLongs: true, promoteValues: true, promoteBuffers: false }, flushing: false, queue: [], writeStream: null, destroyed: false, hashedName: '586bf2335ac81ff4bbeea3baa7d6c991338ee2cf', workItems: [], socket: TLSSocket { _tlsOptions: [Object], _secureEstablished: true, _securePending: false, _newSessionPending: false, _controlReleased: true, _SNICallback: null, servername: 'brucegust-shard-00-00-qhxnz.mongodb.net', alpnProtocol: false, authorized: true, authorizationError: null, encrypted: true, _events: [Object], _eventsCount: 6, connecting: false, _hadError: false, _handle: [TLSWrap], _parent: null, _host: 'brucegust-shard-00-00-qhxnz.mongodb.net', _readableState: [ReadableState], readable: true, _maxListeners: undefined, _writableState: [WritableState], writable: true, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: undefined, _server: null, ssl: [TLSWrap], _requestCert: true, _rejectUnauthorized: false, timeout: 360000, [Symbol(res)]: [TLSWrap], [Symbol(asyncId)]: 35, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: Timeout { _called: false, _idleTimeout: 360000, _idlePrev: [TimersList], _idleNext: [Timeout], _idleStart: 28184, _onTimeout: [Function: bound ], _timerArgs: undefined, _repeat: null, _destroyed: false, [Symbol(unrefed)]: true, [Symbol(asyncId)]: 176, [Symbol(triggerId)]: 35 }, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0, [Symbol(connect-options)]: [Object], [Symbol(disable-renegotiation)]: true }, buffer: null, sizeOfMessage: 0, bytesRead: 0, stubBuffer: null, ismaster: { hosts: [Array], setName: 'brucegust-shard-0', setVersion: 1, ismaster: true, secondary: false, primary: 'brucegust-shard-00-00-qhxnz.mongodb.net:27017', tags: [Object], me: 'brucegust-shard-00-00-qhxnz.mongodb.net:27017', electionId: 7fffffff0000000000000001, lastWrite: [Object], maxBsonObjectSize: 16777216, maxMessageSizeBytes: 48000000, maxWriteBatchSize: 100000, localTime: 2019-05-07T13:30:00.578Z, logicalSessionTimeoutMinutes: 30, minWireVersion: 0, maxWireVersion: 7, readOnly: false, ok: 1, operationTime: [Timestamp], '$clusterTime': [Object] }, lastIsMasterMS: 117 }, message: BinMsg { parsed: true, raw: <Buffer e6 00 00 00 45 0b 92 01 07 00 00 00 dd 07 00 00 00 00 00 00 00 d1 00 00 00 10 6e 00 01 00 00 00 03 6f 70 54 69 6d 65 00 1c 00 00 00 11 74 73 00 02 00 ... >, data: <Buffer 00 00 00 00 00 d1 00 00 00 10 6e 00 01 00 00 00 03 6f 70 54 69 6d 65 00 1c 00 00 00 11 74 73 00 02 00 00 00 73 88 d1 5c 12 74 00 01 00 00 00 00 00 00 ... >, bson: BSON {}, opts: { promoteLongs: true, promoteValues: true, promoteBuffers: false }, length: 230, requestId: 26348357, responseTo: 7, opCode: 2013, fromCompressed: undefined, responseFlags: 0, checksumPresent: false, moreToCome: false, exhaustAllowed: false, promoteLongs: true, promoteValues: true, promoteBuffers: false, documents: [ [Object] ], index: 214, hashedName: '586bf2335ac81ff4bbeea3baa7d6c991338ee2cf' }, ops: [ Product { title: 'Muscular Christianity', price: '10.00', description: ' A great resource ', imageUrl: 'https://images-na.ssl-images-amazon.com/images/I/51jBtyIt5tL._SX331_BO1,204,203,200_.jpg', _id: 5cd18872df9d142bf4f64653 } ], insertedCount: 1, insertedId: 5cd18872df9d142bf4f64653 }
C) Mongo DB Compass (back to top...) Download Mongo Compass by heading out to https://www.mongodb.com/download-center/compass?jmp=hero Once the installer has finished downloading, you'll find the actual ".exe" file in the "C:\Program Files (x86)\MongoDB Compass Installer" directory. Once you've got the app open, you'll now need to connect to your database. To do that, you'll go out to the Mongo webpage where you have your Clusters and click on "Connect." At that point, you'll then click on "Connect with MongoDB Compass" (see image to the right). Answer the questions and then click on the link they provide. If your Compass app is open when you do this, you'll get a prompt that says it "senses" a connnection. Click "yes" and several fields will be populated automatically. Enter your password and then you'll be able to see your database. Click on the "products" collection and you'll see the product you just entered. Cool! D) Retrieving Products (back to top...) 1) Model static fetchAll() { const db = getDb(); //grab your connection return db .collection("products") .find() // you can add parameters like {title: "A great title"} to qualify your search .toArray() // reserved for smaller bits of information .then(products => { console.log(products); return products; }) .catch(err => { console.log(err); }); } ...and here's your Controller: 2) Controller exports.getProducts = (req, res, next) => { Product.fetchAll() // make sure you're referring to the proper method (fetchAll) .then(products => { res.render("shop/product-list", { prods: products, pageTitle: "All Products", path: "/products" }); }) .catch(err => { console.log(err); }); }; Adjust your routes and you're good to go! This works because you didn't change any property names. E) Fetching a Single Product (back to top...) 1) Cursor (back to top...) A cursor is a starting point in a database. A regular SELECT is going to retrieve all of your results and throw it in a table. A cursor is going to "point" to your results and wait for the system to ask for them before they make them available for display. Of course, that's a little inefficient so Mongo makes a compromise and offers the first 101 results by default. In this example, you're going to want to reign in the whole "cursor" dynamic by letting it know you only want one row. We do that with the "next" functionality. All that's going to do is advance the cursor one row and stop. In this case, that's the one and only row that we want. Not that there's any more rows to consider, but that's how you hone in on the one row that's needed in the case of wanting to retrieve a single row. 2) ObjectId (back to top...) Mongo's Id field (_id) - the "Primary Key," so to speak - is represented by the ObjectId. In order to compare that to other variables, you need to introduce the "mongodb" package that will transform your strings into something that can be "understood." For example... In this part of our Controller, we've got this: .find({ _id: prodId }) This won't work because the "_id" field is part of the "ObjectId" dynamic and won't be processed as a string. Hence, the "prodId," doesn't have a chance of being accurately compared to any value in the database. To solve that malady, we do this: .find({ _id: new mongodb.ObjectId(prodId) }) Now, we have a winner! Here's our Model: 3) Model (back to top...)
static findById(prodId) { const db = getDb(); return db .collection("products") .find({ _id: new mongodb.ObjectId(prodId) }) .next() .then(product => { console.log(product); return product; }) .catch(err => { console.log(err); }); }
...and here's our Controller: 4) Controller (back to top...)
Product.findById(prodId) .then(product => { res.render("shop/product-detail", { product: product, pageTitle: product.title, path: "/products" }); }) .catch(err => console.log(err)); };
ONE IMPORTANT NOTE: Our "prodID," both in the "index.ejs" and the "product-list.ejs" file needs to be altered, as far as the "product.id" value to now be the "product._id" value! F) Editing a Product (back to top...) While we can view a single product now for the sake of the product details, now we need to do the same kind of thing from an administrative standpoint so we can edit that product. Here we go: Here's your new Controller with the changes highlighted: 1) Controller (Product Display) (back to top...)
exports.getEditProduct = (req, res, next) => { const editMode = req.query.edit; if (!editMode) { return res.redirect("/"); } const prodId = req.params.productId; Product.findById(prodId) .then(product => { if (!product) { return res.redirect("/"); } res.render("admin/edit-product", { pageTitle: "Edit Product", path: "/admin/edit-product", editing: editMode, product: product }); }) .catch(err => { console.log(err); }); };
Beforehand we were using Sequelize and basing our retrieval on the "user" object, so it looked like this: req.user .getProducts({ where: {id: prodId }) .then(products => { const product = products[0]; Now, we're simply doing a query according to our Mongo database protocol. Everything else is going to be the same, pretty much. Although we do want to make sure that our "id" in our View is documented now as "_id." Also, the "findById" is something we looked at earlier. That will do it, as far as being able to display the product. 2) Controller (Product Edit) (back to top...)
exports.postEditProduct = (req, res, next) => { const prodId = req.body.productId; const updatedTitle = req.body.title; const updatedPrice = req.body.price; const updatedDesc = req.body.description; const updatedImageUrl = req.body.imageUrl; const product = new Product( updatedTitle, updatedPrice, updatedDesc, updatedImageUrl, prodId ); product .save() .then(result => { console.log("Updated Product!"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); res.redirect("/admin/products"); };
1
here's where we're getting our values from what's been posted
2
positioning the values we want to pass into our Model in the proper order
3
we don't have to convert our prodId value just yet. We'll do that in our Model 3) Model (Product Edit) (back to top...) Here's your Model:
const mongodb = require("mongodb"); const getDb = require("../utility/database").getDb; class Product { constructor(title, price, description, imageUrl, id) { this.title = title; this.price = price; this.description = description; this.imageUrl = imageUrl; this._id = new mongodb.ObjectId(id); } save() { const db = getDb(); //grab your connection let dbOp; if (this._id) { console.log("bring it"); //update the product dbOp = db .collection("products") .updateOne({ _id: this._id }, { $set: this }) } else { dbOp = db .collection("products") //choose / establish your collection .insertOne(this) .then(result => { console.log(result); }) .catch(err => { console.log(err); }); } return dbOp .then(result => { console.log(result); }) .catch(err => { console.log(err); }); }
1
you're going to need the "mongodb" package for the sake of the "mongodb.ObjectId" piece.
2
your constructor! Notice the the order of your properties / values are in the same order as your Controller!
3
here's where you do your conversion from you incoming prodId as a string to a Mongo object. Again, click here for a review.
4
looking to see if the "_id" value / variable is present. If it is, then you're go into "edit" mode. If not, you're in "insert" mode.
5
your basic Mongo update code G) Deleting a Product (back to top...) 1) Model (back to top...) This is getting easier! Here's your Model:
static deleteById(prodId) { const db = getDb(); return db .collection("products") .deleteOne({ _id: new mongodb.ObjectId(prodId) }) .then(result => { console.log("deleted!"); }) .catch(err => { console.log(err); }); }
1
grab you database connection
2
standard Mongo delete function. You can read about this and other CRUD operations by referring to the Mongo docs. 2) Controller (back to top...) Your Controller is similar to what you had before with the exception of the way you retrieve the prodId: exports.postDeleteProduct = (req, res, next) => { const prodId = req.body.productId; Product.deleteById(prodId) .then(result => { console.log("product is deleted"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); }; Be sure to reinstate your "delete-product" routes and you'll be set! H) Ternary "IF" Change on save() (back to top...) After deleting the one product that we had, an issue with adding a product surfaced and it was because of the way we positioned our "this._if" dynamic. Initially, we had things set up like this:
class Product { constructor(title, price, description, imageUrl, id) { this.title = title; this.price = price; this.description = description; this.imageUrl = imageUrl; this._id = id ? new mongodb.ObjectId(id) : null; this._id=new mongodb.Object(id); //original code } save() { const db = getDb(); //grab your connection let dbOp; if (this._id) { console.log("bring it"); //update the product dbOp = db .collection("products") .updateOne({ _id: this._id }, { $set: this }); } else { dbOp = db .collection("products") //choose / establish your collection .insertOne(this) .then(result => { console.log(result); }) .catch(err => { console.log(err); }); } return dbOp .then(result => { console.log(result); }) .catch(err => { console.log(err); }); }
Because of the way we had originally set things up, "this._id" was always processed as a value of some kind. Even an "empty" value can be converted to a Mongo Object. By introducing the ternary "IF" statement, we can now return a legitimate "null" value and now have our processes routed correctly so we can insert a new product. I) Adding a User (back to top...) To add a user, you'll first add a new collection in your Mongo database using the "Compass" application. Then you'll go ahead an manually enter a user, just for the sake of drill. Now, let's set up some syntax that looks for a particular user right after the app spins up. 1) Model (user.js) (back to top...) Here's the Model. Pretty straight forward...
const mongodb = require("mongodb"); const getDb = require("../utility/database").getDb; const ObjectId = mongodb.ObjectId; class User { constructor(username, email) { this.name = username; this.email = email; this._id = id ? new mongodb.ObjectId(id) : null; } save() { const db = getDb(); return db.collection("users").insertOne(this); } static findById(userId) { const db = getDb(); return db .collection("users") return db.collection("users").findOne({ _id: new ObjectId(userId) }); } } module.exports = User;
1
a ternary IF statement that assigns the "._id" property with a value if one is being passed in as an argument. Otherwise, it's assigned a "null" value
2
you can use this code: .find({ _id: new ObjectId(userId) }) .next(); ...but if you know you're only going to return one row, "findOne" works just fine In both instances, though, be mindful of the fact that you've converted your incoming value, which is arriving as a string, into a Mongo object - a datatype that Mongo can understand. 2) Controller (app.js) (back to top...) Since we're going for what amounts to a "global" variable here, we're using "app.js" as our Controller. Here's how it looks: app.use((req, res, next) => { User.findById("5cd4670dd87e8f06e4411b6d") .then(user => { req.user = user; console.log(user); next(); }) .catch(err => { console.log(err); }); }); We're just "finding" one that we know to be in existence. The part that is highlighted represents the "user" object. We've got access to every piece of info that's cataloged in our database that belongs to that user. That will come to bear in just a minute. J) Adding a Product w/ User (back to top...) We're now going to edit our "add product" code to include a user id so we can know what user added the product in question. Here's our Controller: 1) Controller (admin.js) (back to top...)
exports.postAddProduct = (req, res, next) => { const title = req.body.title; const price = req.body.price; const description = req.body.description; const imageUrl = req.body.imageUrl; const product = new Product( title, price, description, imageUrl, null, req.user._id ); product .save() .then(result => { console.log("Created Product!"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); };
The highlighted portion shows us including the id of the user (which is a string at this point) is being thrown into the stream of what will be intercepted as a series of properties / constructs in our Model. Remember, "req.user_.id" is coming from our "app.js" file: app.use((req, res, next) => { User.findById("5cd4670dd87e8f06e4411b6d") .then(user => { req.user = user; console.log(user); next(); }) .catch(err => { console.log(err); }); }); Granted, we just went over that, but it bears repeating... 2) Model (product.js) (back to top...) This is an abbreviated version of what the new "product.js" file looks like, but all that's needed to be acknowledged as something new is what you see highlighted in yellow... class Product { constructor(title, price, description, imageUrl, id, userId) { this.title = title; this.price = price; this.description = description; this.imageUrl = imageUrl; this._id = id ? new mongodb.ObjectId(id) : null; this.userId = userId; Basically, we're just grabbing the incoming userId and adding as a property in our Constructor. Perfect. We don't have to set up a new field in the "products" collection. Just run the code and it's all good! K) Cart Items & Orders (back to top...) Right out of the chute, let's go ahead and establish that our Cart is based on the User object. That said, let's take a look at the User Model (user.js): 1) User Model (user.js) (back to top...)
addToCart(product) { const updatedCart = {items: [ {...product, quantity:1 }]}; const db = getDb(); return db .collection('users') .updateOne( {_id: new ObjectId(this._id)}, { $set: {cart: updatedCart } } ); }
1
const updatedCart is an object that holds a property called "items." "items" is an array that holds only one object. That object contains all of the properties contained in the incoming "product" object and then, using the "spread operator," we're overwriting and / or adding the property of "quantity" and its corresponding value of "1." One thing: You can use an alternative way to look to see if the current cart is anywhere in the "users" table... const CartProduct = this.cart.items.findIndex(cp => { return cp._id === product._id; }); this.cart.items refers to the "items" that's a part of the "cart" array. "findIndex" is a JavaScript function that's going to evaluate every item in the "items" array. "cp" represents the products in the "items" array. If "cp._id equals the current product id, then it will return "true." We'll get into that more later, but just be aware of that for now.
2
you're going to update the "cart" value in the "users" collection to the info we've got now assembled in the "updatedCart" object. To do that, you're going to start by grabbing your database connection (getDb) and then focusing on the "users" collection. At that point, you're now going to "updateOne" row that belongs to the row matching the user id that we've got coming into our Class({_id: new ObjectId(this._id)},) and then "setting" ($set) the cart value to the "updatedCart" object. 2) app.js (add more substance to "req.user") (back to top...) initially, when you were using req.user = user; in our "app.js" file, we had this: app.use((req, res, next) => { User.findById("5cd4670dd87e8f06e4411b6d") .then(user => { req.user = user; console.log(user); next(); }) .catch(err => { console.log(err); }); }); When you looked at the output of "console.log(user)," you had this:
{ _id: 5cd4670dd87e8f06e4411b6d, name: 'Bruce', email: 'bruce@brucegust.com' }
That's exactly what you should be getting, However, if you were to pump that data as arguments to our "user" class, you could get more database than just the name and the email address. So, now we've got this:
app.use((req, res, next) => { User.findById("5cd4670dd87e8f06e4411b6d") .then(user => { //req.user = user; req.user = new User(user.name, user.email, user.cart, user._id); /*the reason this is different is because you're invoking the User class which is going to respond with ALL of the info that it is going to retrieve rather than just the info coming from "findById"*/ console.log(req.user.email); next(); }) .catch(err => { console.log(err); }); });
This works! Although, when you take a look at the document that's now in our "users" collection, you'll see all of the product data which is somewhat redundant. To do that, we'll change this: const updatedCart = { items: [{ ...product, quantity: 1 }] }; ...to this: items: [{ productId: new ObjectId(product._id), quantity: 1 }] So, instead of bringing the all of the properties belonging to the "product" object to bear, we'll just grab the "_id." L) Storing Multiple Items in the Cart (back to top...) Here's our code now:
addToCart(product) { const cartProductIndex = this.cart.items.findIndex(cp => { return cp.productId.toString() === product._id.toString(); }); let newQuantity = 1; const updatedCartItems = [...this.cart.items]; if (cartProductIndex >= 0) { newQuantity = this.cart.items[cartProductIndex].quantity + 1; updatedCartItems[cartProductIndex].quantity = newQuantity; } else { updatedCartItems.push({ productId: new ObjectId(product._id), quantity: newQuantity }); } const updatedCart = { items: updatedCartItems }; const db = getDb(); return db .collection("users") .updateOne( { _id: new ObjectId(this._id) }, { $set: { cart: updatedCart } } ); }
1
as has been mentioned before, you're using "findIndex" method which will return the index of the element in the array that matches whatever criteria you specify.
2
you're going to return whatever index ([0], [1], etc) that matches the id of the item being added to the cart. You're also using "toString" in order to ensure that you're comparing apples to apples. The "product._id" is going to be that weird BSON nonsense.
3
establishing a default quantity value
4
creating a duplicate copy of the properties and values belonging to the current cart
5
if the "cartProductIndex" is greater than or equal to zero, we've got a product in our cart that matches the item we're getting ready to add
6
establishing the new quantity value by adding "1" to the current quantity value
7
attaching that new quantity value to the appropriate row in the cart rowset
8
using "push" to add what is now known as a new item to the cart to the existing cart
9
using the Mongo method of "updateOne" to update the current row in the database or to add a new row to the collection M) Displaying Cart (back to top...) To display the cart, we're going into our "users.js" model file and doing this:
getCart() { const db = getDb(); const productIds =this.cart.items.map(i => { return i.productId; }) return db.collection('products').find({_id: {$in: productIds}}).toArray() .then(products => { return products.map(p => { return { ...p, quantity: this.cart.items.find(i => { return i.productId.toString() === p._id.toString(); }).quantity }; }) }); }
Before we break this down, let's take a look at the highlighted code because that represents our target. db.collection('products')... We're doing a SELECT of all of the products. But we only want those products that in our user's cart, so we do a "find" and base our "search criteria" on the productIds that we have in our current cart. That's where the Mongo "find" method comes into play. We want to find all of the products in the "products" collection that match the productIds in our current Cart. Part of what makes this "SELECT" a little different is that we're not just looking for one "_id," we want all of the "_id" values that match all of the "productIds" in the Cart and that's where "$in" comes in handy. It's very similiar to the SQL "in" syntax. It's going to evaluate an array of elements - in this case, productIds - and return the matching values as an array. So, that's what we need in place to evaluate. We need an array of all of the productIds that are in our current cart and that's where we start with ...
1
the JavaScript map method creates a new array with the results of calling a function for every array element. Here's we're taking "items," which is the object in our "user" collection that corresponds to our current user, and returning the productId of every product that's in that array.
2
>return db.collection('products').find({_id: {$in: productIds}}).toArray() - this is the main machine that we commented on a moment ago. We're taking the "productIds" constant that we built in the previous line and using that as a filter via the "$in" dynamic and then, with the "inArray" method, we're converting that result to an array.
3
we've got a cursor pointing to our "products" collection that's comprised of all the products that match what's in our cart. Now, we've got to take those results and attach the appropriate quantity to each one.
4
to attach the appropriate quantity values to each of our products, we use "map" to execute a function on every member of our array which we represent with the letter "p." We start by exacting the "spread operator" on our product array and add a new "product" quantity. That value has to be attached to the correct product in our products array which is the products we have in our cart. That's why... return products.map(p => { return { ...p, quantity: this.cart.items.find(i => { return i.productId.toString() === p._id.toString(); }).quantity }; }) works! "find" is a built in JavaScript function that's going to look at all of the items in the cart and match the product in the product database to the product in the cart. In the end we have the product, but it's the last piece - the ".quantity" part - that we want and that's what we grab to add to our product listing in the Cart. One quick bug fix: Redirect the user to the "cart" after adding an item to the Cart...
exports.postCart = (req, res, next) => { const prodId = req.body.productId; Product.findById(prodId) .then(product => { return req.user.addToCart(product); }) .then(result => { //console.log(result); res.redirect("/cart"); }); };
N) Deleting Cart Items (back to top...) Here's our "user.js" Model file:
deleteItemFromCart(productId) { const updatedCartItems = this.cart.items.filter(item => { return item.productId.toString() !== productId.toString(); }); const db = getDb(); return db .collection("users") .updateOne( { _id: new ObjectId(this._id) }, { $set: { cart: { items: updatedCartItems } } } ); }
1
this.cart.items.filter... First off, just for the sake of review - "this.cart.items." That's coming from your constructor at the top of your Class. In your "app.js" file, you're importing the "User" model and there's a method that's a part of establishing the session user... app.use((req, res, next) => { User.findById("5cd4670dd87e8f06e4411b6d") .then(user => { //req.user = user; req.user = new User(user.name, user.email, user.cart, user._id); /*the reason this is different is because you're invoking the User class which is going to respond with ALL of the info that it is going to retrieve rather than just the info coming from "findById"*/ //console.log(req.user.email); next(); }) .catch(err => { console.log(err); }); }); If the user has added anything to the Cart, that info is going to be a part of the user's class. "items" is going to be an object with two parameters: productId and quantity: productId: new ObjectId(product._id), quantity: newQuantity // line 31-32 from the "user.js" file The "filter" function is something that we've discussed before that's available through regular JavaScript. It's going to filter out all of the items that match the defined criteria. In this case, we're getting all of the items that DON'T match the productId we want to delete.
2
These are the two parameters that we've asserted into our typical Mongo "update" code." { _id: new ObjectId(this._id) }, { $set: { cart: { items: updatedCartItems } } } ); The first is the "productId," which qualifies which document we're actually targeting.
3
The second parameter starts with the "$set" character. IT's updating the "cart" object and specifically highlighting the "items" object that's a part of the cart. And that's how it gets done! O) Adding an Order (back to top...) 1) users.js (model) (back to top...)
addOrder() { const db = getDb();
2
return this.getCart() .then(products => { const order = { items: products, user: { _id: new ObjectId(this._id), name: this.name, email: this.email } }; return db.collection("orders").insertOne(order); }) .then(result => { this.cart = { items: [] }; return db .collection("users") .updateOne( { _id: new ObjectId(this._id) }, { $set: { cart: { items: [] } } } ); }); }
1
I'm establishing my database connection
2
you'll need to return the result of the "then" block so your controller has a result to publish. That part of the Controller looks like this: exports.getOrders = (req, res, next) => { req.user .getOrders() .then(orders => { res.render("shop/orders", { path: "/orders", pageTitle: "Your Orders", orders: orders }); }) .catch(err => console.log(err)); }; In addition, the result of the "getCart" is going to be all of the data that's coming from the "getCart" method which includes the productId and the quantity. You'll throw that in the "items" object and then populate the user object with the properties and values coming from the stored user id. Like this... "const order = { }" - when you see that, know that what's positioned between the two curly braces is an "object." An object, by definition, is a collection of properties and values, although you can have an object within an object as well. You see that here. In this case, "items" is a property and "products" is your value, which in this example, just happens to be an array. "user" is an object with the properties being "_id," "name" and "email" and the values being what's coming from the the properties and values that came from the constructor at the top of the class: class User { constructor(username, email, cart, id) { this.name = username; this.email = email; this.cart = cart; // {items: []} this._id = id; }
3
the "return" that you used with
1
was for the sake of your Controller so it had something to display. This "return" is just a part of your typical Mongo insertion syntax.
4
this "then" block is happening in the case that the first "then" block was completed successfully. In this case what's happening is your emptying the items that correspond to this user's cart as well as the cart items that were documented in the "users" table. 2) shop.js (controller) (back to top...) exports.postOrder = (req, res, next) => { req.user .addOrder() .then(result => { res.redirect("/orders"); }) .catch(err => { console.log(err); }); }; This is really straight forward. You're grabbing the "addOrder" method as per the current user object, doing what needs to be done and then, based on a positive result, sending the user to the "orders" page. A) ORM vs ODM (back to top...) ORM stands for "Object Relational Mapping Library." ODM stands for "Object Document Mapping Library." The bottom line is that Mongoose is to Mongo what Sequelize is to SQL. You're able to interact with you database a lot more efficiently in that you're not having to write as much code. B) Installing Mongoose and Connecting to the Database (back to top...) 1) Install Mongoose (back to top...) First of all, you can access the Mongoose docs by heading out to https://mongoosejs.com/docs/guide.html. You install Mongoose like this:
npm install --save mongoose
2) Using Mongoose to Connect to the Database (back to top...) Mongoose takes care of your connection in a very convenient way! This is the code you use in your "app.js" file! mongoose .connect( '"mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/test?retryWrites=true' ) .then(result => { app.listen(3000); }) .catch(err => { console.log(err); }); C) Creating the Schema (back to top...) Mongo is "schema-less!" So why do we kick things off with a schema? Because it's a good starting point. Plus, it can be overriden, although in the example below you'll see how we made all of our fields required. That can't be overriden. Still, we get some flexibility that's worth a mild compromise that we're making by establishing these fields as required. This is the "product.js" model...
const mongoose = require('mongoose'); const Schema = mongoose.Schema; const productSchema = new Schema({ title: { type: String, required: true }, price: { type: Number, required: true }, description: { type: String, required: true }, imageUrl: { type: String, required: true } });
D) Save a Product (back to top...) Here's what your "admin.js" Controller. Notice that we're not needing a Model! You'll see that explanation at
2
!
exports.postAddProduct = (req, res, next) => { const title = req.body.title; const price = req.body.price; const description = req.body.description; const imageUrl = req.body.imageUrl; const product = new Product({ title: title, price: price, description: description, imageUrl: imageUrl }); product .save() .then(result => { console.log("Created Product!"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); };
1
you're using pretty much the same code as you were with straight Mongo with the exception of using a JavaScript object here at "new Product." You're just hooking things up as key / value pairs.
2
the "save" function, here, is something that's coming from Mongoose and not a separate function that you're having to call from a Model and a Mongo function listed separately. E) Fetch All Products (back to top...) Again, we don't have to concern ourselves with a Model. This is the "shop.js" Controller...
exports.getProducts = (req, res, next) => { Product.find() //mongoose method .then(products => { console.log(products); res.render("shop/product-list", { prods: products, pageTitle: "All Products", path: "/products" }); }) .catch(err => { console.log(err); }); };
One example of just how cool Mongoose can be. The code is pretty much the same, but instead of having to rely on a Mongoose based method in the Model, we can just use the Mongoose "find()" function. F) Fetch A Single Product (back to top...) This, again, is real easy given the fact that "findById" is actually a Mongoose function so all we need to do is adjust the "shop.js" Controller. exports.getProduct = (req, res, next) => { const prodId = req.params.productId; Product.findById(prodId) .then(product => { res.render("shop/product-detail", { product: product, pageTitle: product.title, path: "/products" }); }) .catch(err => console.log(err)); }; G) Edit A Product (back to top...) Again, just the Controller! This is "admin.js..."
exports.postEditProduct = (req, res, next) => { const prodId = req.body.productId; const updatedTitle = req.body.title; const updatedPrice = req.body.price; const updatedDesc = req.body.description; const updatedImageUrl = req.body.imageUrl; Product.findById(prodId) .then(product => { product.title = updatedTitle; product.price = updatedPrice; product.description = updatedDesc; product.imageUrl = updatedImageUrl; return product.save(); }) .then(result => { console.log("Updated Product!"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); };
H) Delete A Product (back to top...) To delete a product, again, all you'll need is the "admin.js" Controller since the "product.js" Model is nothing more than the schema, but... ...it's documented in the context of a Mongoose which is what all of this stuff flows as easilyl as it does. Here's your "admin.js" Controller:
exports.postDeleteProduct = (req, res, next) => { const prodId = req.body.productId; Product.findByIdAndRemove(prodId) .then(result => { console.log("product is deleted"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); };
I) Adding and Using a User Model (back to top...) 1) user.js Model (back to top...) Here's your "user.js" Model. Notice, again, that it's just a schema. Also, notice how when we go to export this, it's module.exports = mongoose.model("User", userSchema); The "User" portion is going to be processed by Mongoose as the name of the collection, but in all lower case letters and the plural form of the word. So, the resulting table is going to be "users."
const mongoose = require("mongoose"); const Schema = mongoose.Schema; const userSchema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, cart: { items: [ { productId: { type: Schema.Types.ObjectId, required: true }, quanity: { type: Number, required: true } } ] } }); module.exports = mongoose.model("User", userSchema);
2) app.js (back to top...) So, here's our "app.js" file. Similar concept as what we were doing with Mongo, just using the "findById" method that comes with Mongoose.
app.use((req, res, next) => { User.findById("5cddf37ccb51de2c140c3638") .then(user => { req.user = user; next(); }) .catch(err => { console.log(err); }); });
Later on in the same file, we assert that user just before we establish the "listen" code:
mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/test?retryWrites=true" ) .then(result => { User.findOne().then(user => { if (!user) { const user = new User({ name: "Bruce", email: "bruce@brucegust.com", cart: { items: [] } }); user.save(); } }); app.listen(3000); }) .catch(err => { console.log(err); });
1
you're defaulting to what amounts to nothing more than the schema at this point. "findOne" is a Mongoose function. If there's not criteria specifiefd, "findOne" will just return the first document in the collection.
2
In this instance, if there is no user in the collection, then a new one is created. J) Using Relations in Mongoose (back to top...) Pretty straight forward and downright easy when you compare it to its Mongo counterpart! Let's start with the "product.js" Model... 2) product.js (back to top...)
const mongoose = require("mongoose"); const Schema = mongoose.Schema; const productSchema = new Schema({ title: { type: String, required: true }, price: { type: Number, required: true }, description: { type: String, required: true }, imageUrl: { type: String, required: true }, userId: { type: Schema.Types.ObjectId, ref: "User", required: true } }); module.exports = mongoose.model("Product", productSchema);
You're adding the highlighted column to your schema and by using the "type" and the "ref," you're automatically defining the relationship between the "users" and the "products" table. Remember a "relationship" is technically defined as an "embedded" document when you're talking about a Mongo database. You'll here "relationship," but just bear in mind that "relationship" is typically used to describe the "join" dynamic betwee two databases in a RDMBS (Relational Database Management System). Just to further break down the above. The type: Schema.Types.ObjectId could conceivably be anything, but when you include ref: "User", you're letting the Mongoose system to set up an embedded document scenario with the "users" table (module.exports = mongoose.model("User", userSchema); [from the "user.js" model). That's all you need to do! 3) admin.js (back to top...) Here's your "admin.js" file:
exports.postAddProduct = (req, res, next) => { const title = req.body.title; const price = req.body.price; const description = req.body.description; const imageUrl = req.body.imageUrl; const product = new Product({ title: title, price: price, description: description, imageUrl: imageUrl, userId: req.user }); product .save() .then(result => { console.log("Created Product!"); res.redirect("/admin/products"); }) .catch(err => { console.log(err); }); };
1
"req.user" is coming from your "app.js" file. For the sake of review, feel free to click here to see how that's generated. Bottom line is that you have a "req.user" object generated on line #23. When you get to the bottom of the page, part of the code that establishes the database connection looks for a document in the "users" collection. If there's one there, it moves on. Understand that piece of code exists only for the sake of the app. You generate a new document only if there's nothing in the database and then you've got to hardcode the "_id" into the code on line #23.
Be aware that the code that fires up the connection etc only fires at "npm start." Your middleware function ("req," "res," "next") is what fires throughout the course of the application. Also, know that "req.user" contains more information than just the userId, but Mongoose knows just to grab that value.
2
product.save is making use of the Mongoose "save" functionality that's being coupled to the "product" model / schema. K) Additional Notes for Managing Relations in Mongoose (back to top...) 1) Retrieving the Entire User Object (back to top...) When you do this: exports.getProducts = (req, res, next) => { Product.find() .then(products => { console.log(products); res.render("admin/products", { prods: products, pageTitle: "Admin Products", path: "/admin/products" }); }) .catch(err => { console.log(err); }); }; You get this:
{ _id: 5cded351dd7833346cadaf65, title: 'The Greatest Bible Study in the World', price: 10.09, description: 'A great Bible study ', imageUrl: 'https://images-na.ssl-images-amazon.com/images/I/51OiIgcqMIL._SX331_BO1,204,203,200_.jpg', userId: 5cddf37ccb51de2c140c3638, __v: 0 } ]
Notice the "userId." That's the only piece of the "user" data that you're going to get because that's all that's included in the "products" collection. However, if you wanted to get all of the "user" data, you could do this: exports.getProducts = (req, res, next) => { Product.find() .populate("userId") .then(products => { console.log(products); res.render("admin/products", { prods: products, pageTitle: "Admin Products", path: "/admin/products" }); }) .catch(err => { console.log(err); }); }; Now look at what you have:
{ _id: 5cded351dd7833346cadaf65, title: 'The Greatest Bible Study in the World', price: 10.09, description: 'A great Bible study ', imageUrl: 'https://images-na.ssl-images-amazon.com/images/I/51OiIgcqMIL._SX331_BO1,204,203,200_.jpg', userId: { cart: [Object], _id: 5cddf37ccb51de2c140c3638, name: 'Bruce', email: 'bruce@brucegust.com', __v: 0 }, __v: 0 } ]
Now you've got all of the user data. 2) Specifying Which Data You Want to Retrieve (back to top...) You can also specify which data you want to retrieve. Like this:
exports.getProducts = (req, res, next) => { Product.find() .select("title price -_id") .populate("userId", "name") .then(products => { console.log(products); res.render("admin/products", { prods: products, pageTitle: "Admin Products", path: "/admin/products" }); }) .catch(err => { console.log(err); }); };
1
with .select(title price -_id"), you're limiting what's being displayed to "title" and "price" and you're also explicitly eliminating the "_id" field.
2
with .populate("userId", "name") you're restricting the "user" object to just those two properties
[ { title: 'Muscular Christianity', price: 10.01 }, { title: 'The Greatest Bible Study in the World', price: 10.09, userId: { _id: 5cddf37ccb51de2c140c3638, name: 'Bruce' } } ]
BOOM! L) Adding User Data to the Shopping Cart (back to top...) Now we're going to add the user's information to the cart when they order something. 1) shop.js Controller (back to top...) Here's the shop.js Controller:
exports.postCart = (req, res, next) => { const prodId = req.body.productId; Product.findById(prodId) .then(product => { return req.user.addToCart(product); }) .then(result => { console.log(result); res.redirect("/cart"); }); };
1
"return" means that you're taking an intentional left hand turn where the flow of your function is concerned. It ends the execution of a statement and specifies a value. In this case, you're getting ready to specify a value based on what comes back from the "addToCart" method.
In Sequelize, we had something similar in that we set up the "req.user" value in the "app.js" file and when we did that we were automatically given a number of methods that we could execute with the "req.user" object. Reason being is that it was more than just a JavaScript object, it was a Sequelize object that was stuffed with all kinds of values and functionality. Here we have something similiar in that we've got the "req.user" object and we can go through that object and execute any one of a number of methods.
2) user.js Model (back to top...)
userSchema.methods.addToCart = function(product) { const cartProductIndex = this.cart.items.findIndex(cp => { return cp.productId.toString() === product._id.toString(); }); let newQuantity = 1; const updatedCartItems = [...this.cart.items]; if (cartProductIndex >= 0) { newQuantity = this.cart.items[cartProductIndex].quantity + 1; updatedCartItems[cartProductIndex].quantity = newQuantity; } else { updatedCartItems.push({ productId: product._id, quantity: newQuantity }); } const updatedCart = { items: updatedCartItems }; this.cart = updatedCart; return this.save(); };
1
userSchema.methods... this is code that is understood by Mongoose as a method that's tailored for this particular Model. In this case, we're looking at the "user" model so "addToCart" is going to be referenced as such with return req.user.addToCart(product);. That's the code that we used in our "user.js" Controller. (product) is going to be the product object we passed into the method in our "user.us" controller as an argument.
2
as has been mentioned before, you're using "findIndex to find the index of the element you're looking for which, in this case, is the "this" index of the current cart's products.
3
if you find a matching productId, you return that value and the loop stops with the value that's being returned
4
establish a new variabgle representing the starting point for whatever quantity value you have
5
use the spread operator to create a duplicate of the items in your current cart object
6
if you've got an index value greater than zero, they you increase the current quanity value by 1 and then you update the items object with the new quantity value
7
if you're dealing with a new product, then you're going to update the "updatedCartItems" object with the new productId and quantity value
8
up to this point, you're been working with the "items" object. You need to reposition that as a property within the "cart" object. Now you can update it correctly. M) Loading the Cart w/ Product Information (populate) (back to top...) Here's we're going to use a featuring in Mongoose called "populate." Here's your code from the "shop.js" Controller:
exports.getCart = (req, res, next) => { req.user .populate("cart.items.productId") .execPopulate() .then(user => { const products = user.cart.items; res.render("shop/cart", { path: "/cart", pageTitle: "Your Cart", products: products }); }) .catch(err => console.log(err)); };
"populate" leverages the fact that you've got an embedded "product" document in the "users" collection. It's basically used to "populate" the data within your reference. If we look at what gets logged into our console, we get this:
{ _id: 5cdf4f9fd5e2a83574c574f3, productId: { _id: 5cded351dd7833346cadaf65, title: 'The Greatest Bible Study in the World', price: 10.09, description: 'A great Bible study ', imageUrl: 'https://images-na.ssl-images-amazon.com/images/I/51OiIgcqMIL._SX331_BO1,204,203,200_.jpg', userId: 5cddf37ccb51de2c140c3638, __v: 0 }, quantity: 3 },
Notice the "productId" object. See how we're getting all of our "product" information. You can see how "populate" is working. That being the case, when we go out to our "cart.ejs" file, we've got a to make a couple of changes because of how the data is being stacked. This is only part of the code. The changes have been highlighted: <% if(products.length >0) { %> <ul class="cart__item-list"> <% products.forEach(p => { %> <li class="cart__item"> <p></p><%=p.productId.title %> (<%=p.quantity%>)</p> <form action="/cart-delete-item" method="Post"> <input type="hidden" name="productId" value="<%=p.productId._id %>"> <button class="btn" type="submit">Delete</button> </form> </li> <% }) %> Now, it will fly because we've addressed the way that data is now "stacked" in the incoming recordset. N) Delete Cart Item(back to top...) Here's your code. This is going to be in the "user.js" Controller file:
userSchema.methods.removeFromCart = function(productId) { const updatedCartItems = this.cart.items.filter(item => { return item.productId.toString() !== productId.toString(); }); this.cart.items = updatedCartItems; return this.save(); };
This is comparable to what we've done in the past as far as creating a copy of the current cart, filtering out all of the items that do NOT equal the productId of the selected item and everything that's left over is saved. O) Add Order (back to top...) First, we need to add a "user.js" for the sake of a new table. That code looks like this: 1) user collection (back to top...)
const mongoose = require("mongoose"); const Schema = mongoose.Schema; const orderSchema = new Schema({ products: [ { //an array of documents product: { type: Object, required: true }, quantity: { type: Number, required: true } } ], user: { name: { type: String, required: true }, userId: { type: Schema.Types.ObjectId, require: true, ref: "User" } } }); module.exports = mongoose.model("Order", orderSchema);
Now, here's the "shop.js" Controller (we imported the new "user" model at the top of the page):
exports.postOrder = (req, res, next) => {
req.user .populate("cart.items.product") .execPopulate() .then(user => { const products = user.cart.items.map(i => { return { quantity: i.quantity, product: { ...i.productId._doc }; });
const order = new Order({ //needs to be initialized user: { name: req.user.name, userId: req.user }, products: products }); return order.save(); }) .then(result = { req.user.clearCart(); } .then(result => { res.redirect("/orders"); }) .catch(err => { console.log(err); }); };
We're taking everything from the current cart that corresponds to this user and creating a new object that contains that info and inserting that into our users table.
1
This first part of the code that's in the "dotted line" box, creates a "products" object that includes all of the product data along with the correct quanities based on what's currently in the cart. First thing you're doing is populating the "orders" collection with the product information that corresponds to the products in the current cart. "populate" has been covered before with Retrieving the Entire User Object Basically, it's very similiar to a join. We're grabbing all of the product info that corresponds to the productId, "embedding" all of it into the "users" table. So, whereas before we might've had just the productId, now we have the "title," the "productId" as well as the "description" etc. Click here for more information. Bear in mind that in your "users" table, your product information will all be stored as an object that's associated with the productId value.
In the past, when we went to embed product information into the cart object in the "users" table, we grabbed the product information using "findById" and then inserting the result. It's intuitive, but with "popluate," you get all of that info with half the syntax. It's much like a "join," but with a whole lot less effort.
2
although it may seem weird that "req.user" and "user" are the same, remember that "user" gets established when you first fire up the app in "app.js..."
app.js 
mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/test?retryWrites=true" ) .then(result => { User.findOne() .then(user => { if (!user) { const user = new User({ name: "Bruce", email: "bruce@brucegust.com", cart: { items: [] } }); user.save(); } }); app.listen(3000); }) .catch(err => { console.log(err); });
"req.user" gets established later... app.use((req, res, next) => { User.findById("5cddf37ccb51de2c140c3638") .then(user => { //req.user = user; req.user = user; next(); }) .catch(err => { console.log(err); }); }); The reason for this, I'm assuming, is for training purposes since there aren't any other users. But, for now, just know that the two are the same.
3
we begin by setting up a constant called "products." "products" is going to be an object that holds two properties and their corresponding values. Map, if you remember, creates a new array with the results of calling a function for every array element. We're going through all of the "items" in our user's document in the "users" collection. It looks like this: _id: 5cddf37ccb51de2c140c3638 name: "Bruce" email:"bruce@brucegust.com" cart: Object items: Array 0: Object _id: 5ce8136eae40ce3714edeb06 productId: 5cdda90bd2b7fb230c24b6b4 quantity: 1 Notice, the "items" array. The "map" will go through every object in that array and, in this case, create an array that will then be introduced into the "orders" collection and it will look like this:
The red arrow is what results from . The blue arrow is what results from: const products = user.cart.items.map(i => { return { quantity: i.quantity, product: { ...i.productId._doc } }; }); You see how it's working? The returned value is an object with a "quantity" property and a "product" property. The "quantity" property, according to the "items" object, is going to have a value of "1," and the "product" property is going to be an array of all the productd properties and values associated with the productId. There's one thing about this code that's worth looking at and that's the "_doc." This is a feature that Mongoose offers, although there's not a whole lot of documentation out there that explains it. To understand it, let's go back to the way this code was originally written. At first, we did this: const products = user.cart.items.map(i => { return { quantity: i.quantity, product: productId }; }); "productId" does, in fact, hold all of the data that's associated with that id in the user's table, but only because of the relationship that exists between the "user" and the "product" collection. When you go to the complete product profile, though, to store it into the "order" collection, you only get the productId like what you see to the right. To get to the data that's embedded, you want to use "_doc." It's going to look like this: const products = user.cart.items.map(i => { return { quantity: i.quantity, product: { ...i.productId._doc } }; }); You turn the "i.productId" entity into a JavaScript object by using the spread operator and then the "_doc" method. Now, you're able to pop the hood on the table relationships that exist between the "user" and the "product" collection and get the data you want to insert into the "cart" collection. Compare the difference between the "products" array in the first example and what you have to the right.
4
start up a new instance of the "orders" collection. You're adding a "users" object and another object which is the "products" array.
5
save the order
6
clear the current cart associated with the current user object
7
redirect the user back to the "orders" table

Objects in JavaScript
P) Display Orders (back to top...) Pretty straight forward. Here's the "shop.js" Controller: exports.getOrders = (req, res, next) => { Order.find({ "user.userId": req.user._id }) .then(orders => { res.render("shop/orders", { path: "/orders", pageTitle: "Your Orders", orders: orders }); }) .catch(err => console.log(err)); }; ...and getOrders is an automatic method provided by Mongoose. A) Cookie Defined (back to top...) Data that's stored in the Browser - the Client Side. B) Add Login Button (back to top...) This was something I wanted to document, just because it's a quick and easy way to position a button on the far right side of the screen while simultaneously having buttons on the left side (see image to the right). Here's the CSS for the navbar: .main-header__nav { height: 100%; width: 100%; display: flex; align-items: center; justify-content: space-between; } Note the changes... And then here's the actual "navbar.js" file:
<div class="backdrop"></div> <header class="main-header"> <button id="side-menu-toggle">Menu</button> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"> <a class="<%= path === '/' ? 'active' : '' %>" href="/">Shop</a> </li> <li class="main-header__item"> <a class="<%= path === '/products' ? 'active' : '' %>" href="/products" >Products</a > </li> <li class="main-header__item"> <a class="<%= path === '/cart' ? 'active' : '' %>" href="/cart">Cart</a> </li> <li class="main-header__item"> <a class="<%= path === '/orders' ? 'active' : '' %>" href="/orders" >Orders</a > </li> <!--<li class="main-header__item"> <a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product">Add Product </a> </li> <li class="main-header__item"> <a class="<%= path === '/admin/products' ? 'active' : '' %>" href="/admin/products">Admin Products </a> </li>--> </ul> <ul class="main-header__item-list"> <li class="main-header__item"> <a href="/login">Login</a> </li> </ul> </nav> </header>
B) Creating the Login Form (back to top...) 1) Create Your Route (back to top...) We're setting up a brand new route file to handle everything that can be categorized as a secure transaction between the user and the site. const rootDir = require("../utility/path"); const router = express.Router(); router.get("/login", (req, res, next) => { }); module.exports = router; 2) Register Your Route in app.js (back to top...) const authRoutes = require("./routes/auth"); ... app.use(authRoutes); 3) Create Your Controller (back to top...) exports.getLogin = (req, res, next) => { res.render("auth/login", { path: "/login", pageTitle: "Login" }); }; This is pointing to your View located in "auth/login..." 4) Create Your View (back to top...) This is "login.ejs" located in views -> auth -> login.ejs
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/auth.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <form class="login-form" action="/login" method="POST"> <div class="form-control"> <label for="title">Email</label> <input type="email" name="email" id="email"> </div> <div class="form-control"> <label for="password">Password</label> <input type="password" name="password" id="password"> </div> <button class="btn" type="submit">Login</button> </form> </main> <%- include('../includes/end.ejs') %>
C) Creating a Cookie (back to top...) So, you can potentially create a variable that is then passed on through the rest of the app similiar to what you did with "user" in your "app.js" file: app.use((req, res, next) => { User.findById("5cddf37ccb51de2c140c3638") .then(user => { req.user = user; next(); }) .catch(err => { console.log(err); }); }); The problem, however, is two-fold. First, when you do this: exports.postLogin = (req, res, next) => { req.isLoggedIn = true; res.redirect("/"); }; ...it doesn't work because once you do a redirect, you're dealing with an entirely new "request" object. So everything you've specified previously is now dead. It works within your "app.js" dynamic because it's a global entity. There's no "redirect" in your "app.js" code that kills the "req.user" value so it remains intact. To get around that, you create a cookie and you use Express to get it done. Like this: exports.postLogin = (req, res, next) => { res.setHeader("Set-Cookie", "loggedIn=true"); res.redirect("/"); }; Notice you're not just setting a variable that will be eliminated once you do a redirect. By using res.setHeader, you're embedding info into the browser that remains intact throughout the user's session. When you adjust your "getLogin" method in your "auth.js" Controller to this: exports.getLogin = (req, res, next) => { console.log(req.get("Cookie")); res.render("auth/login", { path: "/login", pageTitle: "Login", isAuthenticated: req.isLoggedIn }); }; You see this in your console:
_ga=GA1.1.2029811492.1513464095; _gcl_au=1.1.1651456037.1551372552; kampyle_us erid=3621-f0d8-567d-47cf-953b-4b29-f67c-40c9; kampyleUserSession=1551372552335 ; kampyleUserSessionsCount=1; _mkto_trk=id:107-FMS-070&token:_mch-localhost-15 51372552354-60277; kampyleSessionPageCounter=2; fbm_462581780429154=base_domain=; loggedIn=true
Notice that you've got more than one Cookie we're needing, we have to streamline things a bit and we can do it like this... First of all, let's take a look at our Headers. To do that, open up your Dev Tools in Chrome. Click on any page and then do an "inspect." Open up the "Network" tab and then click on the page that you just accessed. Like this:
You'll notice that you've got several cookies...
Cookie: _ga=GA1.1.2029811492.1513464095; kampyle_userid=3621-f0d8-567d-47cf-953b-4b29-f67c-40c9; kampyleUserSession=1551372552335; kampyleUserSessionsCount=1; _mkto_trk=id:107-FMS-070&token:_mch-localhost-1551372552354-60277; kampyleSessionPageCounter=2; fbm_462581780429154=base_domain=; loggedIn=true
To get to the value of our "loggedIn" cookie, we do this: const isLoggedIn = req .get("Cookie") .split(";")[7] .trim() .split("=")[1]; It looks complicated, but we're basically just splitting the initial "pile" of cookies to get to the one we need and then we're splitting that key / value pair so we can get to the "true" value. Now, isLoggedIn is a const with a discernible value attached to it. D) Manipulating a Cookie (Max-Age, Secure, HttpOnly) (back to top...) You can also manipulate your cookies in some useful ways. For example, you can set an expiration date / time: res.setHeader("Set-Cookie", "loggedIn=true; Max-Age=10"); You can also dictate whether or not a user can login based on if the server is secure: res.setHeader("Set-Cookie", "loggedIn=true; Secure"); Another useful metrix is "HttpOnly." res.setHeader("Set-Cookie", "loggedIn=true; HttpOnly"); This prevents any cookie info from being accessible from the browser. This is especially relevant to the prevention of "Cross Site Scripting." Click here to read more. E) Sessions (back to top...) The difference between Cookies and Sessions can be summed up by saying that your Cookies are stored in your Browser whereas your Session variables are going to be stored on the server side (backend). Also, the Session ID will be exclusive to that user. Unlike a cookie that's stored on the browser that can be manipulated, server side variables can't be impacted in the same way. We're still going to make use of the Cookie dynamic, but it will be encrypted so only the server can verify it as being authentic.
F) Installing Session Middleware (back to top...) We're going to install "express-session" by running npm install --save express-session. After that, we'll introduce the following lines of code in our "app.js" file: const session = require("express-session"); ...and app.use( session({ secret: "my secret", resave: false, saveUninitialized: false }) ); To get more info about the various settings you can utilize at this point, click here. The most important setting is the "secret," which should be a long line of text. This will be used as part of your "hash" value. G) Using Session Middleware (back to top...) Now that we've got our session middleware installed, now we'll actually put it to use.
exports.getLogin = (req, res, next) => { console.log(req.session.isLoggedIn); res.render("auth/login", { path: "/login", pageTitle: "Login", isAuthenticated: false }); }; exports.postLogin = (req, res, next) => { req.session.isLoggedIn = true; res.redirect("/"); };
The highlighted section shows how you can establish any session variable that you want, set it to "true" and you're gold! Again, this will be unique to every user and it's stored on the server side of the equation so it can't be manipulated by the user themselves. H) Using MongoDB to Store Sessions (back to top...) Now that we can create Session data, the next thing is to store it in the database. To do that, we'll use a very cool utility that Express provides called, "MongoDB Session." If you head out here, you can see the different packages that Express can use to store session variables in different types of databases. Of course, we're going to use the "MongoDB" option. To install it, we do this:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ npm install --save connect-mongodb-session
1) app.js (back to top...) Once you're got it installed, you'll first import the "MongoDB Session" package into the "app.js" page and create a new constant that holds your Mongo database connection info. Like this:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const mongoose = require("mongoose"); const session = require("express-session"); const MongoDBStore = require("connect-mongodb-session")(session); const MONGODB_URI = "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/test"; const app = express(); const store = new MongoDBStore({ uri: MONGODB_URI, collection: "sessions" }); app.set("view engine", "ejs"); app.set("views", "views"); const adminData = require("./routes/admin"); const shopRoutes = require("./routes/shop"); const authRoutes = require("./routes/auth"); const errorController = require("./controllers/error"); const User = require("./models/user"); app.use(bodyParser.urlencoded()); app.use(express.static(path.join(__dirname, "public"))); app.use( session({ secret: "my secret", resave: false, saveUninitialized: false, store: store }) ); app.use((req, res, next) => { User.findById("5cddf37ccb51de2c140c3638") .then(user => { //req.user = user; req.user = user; next(); }) .catch(err => { console.log(err); }); }); app.use("/admin", adminData.routes); app.use(shopRoutes); app.use(authRoutes); app.use(errorController.get404); mongoose .connect(MONGODB_URI) .then(result => { User.findOne().then(user => { if (!user) { const user = new User({ name: "Bruce", email: "bruce@brucegust.com", cart: { items: [] } }); user.save(); } }); app.listen(3000); }) .catch(err => { console.log(err); });
1
this is the middleware we installed earlier that gives us the ability to set up session variables
2
import the "MongoDB Session" package. Again, this is what allows us to store session data in a Mongo database
3
set up a constant called "MONGODB_URI" to store your connection data. By the way, URI stands for "Uniform Resource Identifier."
4
establishing "store" as a constant that holds the machine that will store our session variables. This is all done automatically and is too, too cool!
5
here's where we're using our "store" constant to store the session id
6
this isn't anything new, we just replaced the original database login info with the new MONGODB_URI constant. 2) auth.js (back to top...) Here's the code that allows you to see what's being stored in the database: exports.getLogin = (req, res, next) => { console.log(req.session.isLoggedIn); res.render("auth/login", { path: "/login", pageTitle: "Login", isAuthenticated: false }); }; exports.postLogin = (req, res, next) => { //res.setHeader("Set-Cookie", "loggedIn=true; Max-Age=10"); req.session.isLoggedIn = true; res.redirect("/"); }; ...and here's what it looks like in Compass (see image to the right): BTW: The Cookie is established automatically by Express. I) Deleting a Cookie (Logout) (back to top...) To logout, you want to create a button that triggers the Express method that "destroys" your current session. You'll do it like this... 1) nav.ejs (your button) (back to top...) Here's your button: <li class="main-header__item"> <form action="/logout" method="post"> <button type="submit">Logout</button> </form> </li> 2) auth.js (your route) (back to top...) Here's your route: router.post("/logout", authController.postLogout); 3) auth.js (your Controller) (back to top...) ...and here's your Controller: exports.postLogout = (req, res, next) => { req.session.destroy(err => { console.log(err); res.redirect("/"); }); }; J) Fixing Some Bugs (back to top...) When you don't have a valid session, you're going to run into problems. Here's how we fixed that: 1) navigation.ejs (back to top...) This is an incremental yet an important fix. We're going to limit the display of some links based on the presence of a valid session id. In other words, you have to be logged in before any of these links show up. Refer to the highlighted sections of the code below to see the changes.
<div class="backdrop"></div> <header class="main-header"> <button id="side-menu-toggle">Menu</button> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"> <a class="<%= path === '/' ? 'active' : '' %>" href="/">Shop</a> </li> <li class="main-header__item"> <a class="<%= path === '/products' ? 'active' : '' %>" href="/products" >Products</a > </li> <li class="main-header__item"> <a class="<%= path === '/cart' ? 'active' : '' %>" href="/cart">Cart</a> </li> <% if (isAuthenticated) { %> <li class="main-header__item"> <a class="<%= path === '/orders' ? 'active' : '' %>" href="/orders" >Orders</a > </li> <li class="main-header__item"> <a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product" >Add Product </a> </li> <li class="main-header__item"> <a class="<%= path === '/admin/products' ? 'active' : '' %>" href="/admin/products" >Admin Products </a> </li> <% } %> </ul> <ul class="main-header__item-list"> <% if (!isAuthenticated) { %> <li class="main-header__item"> <a class="<%= path === '/login' ? 'active' : '' %>" href="/login" >Login</a > </li> <% } else { %> <li class="main-header__item"> <form action="/logout" method="post"> <button type="submit">Logout</button> </form> </li> <% } %> </ul> </nav> </header> <nav class="mobile-nav"> <ul class="mobile-nav__item-list"> <li class="mobile-nav__item"> <a class="<%= path === '/' ? 'active' : '' %>" href="/">Shop</a> </li> <li class="mobile-nav__item"> <a class="<%= path === '/products' ? 'active' : '' %>" href="/products" >Products</a > </li> <li class="mobile-nav__item"> <a class="<%= path === '/cart' ? 'active' : '' %>" href="/cart">Cart</a> </li> <li class="mobile-nav__item"> <a class="<%= path === '/orders' ? 'active' : '' %>" href="/orders" >Orders</a > </li> <li class="mobile-nav__item"> <a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product" >Add Product </a> </li> <li class="mobile-nav__item"> <a class="<%= path === '/admin/products' ? 'active' : '' %>" href="/admin/products" >Admin Products </a> </li> </ul> </nav>
Now, only the admin links are displayed if the user is authenticated. Also, you don't see "logout" unless you're logged in and vice versa. You also see that with the "add to cart" buttons. 2) Display Cart (back to top...) When you went to display the Cart, you got an error that said, "req.session.user.addToCart is not a function." The reason for that error is because you no longer have a "req.session.user" like you did before. We were storing the user in the "req" object. Now, it's in the database as a session. The "store" function that we've got in the "app.js" file... app.use( session({ secret: "my secret", resave: false, saveUninitialized: false, store: store }) ); ...isn't grabbing the info that we need. To fix that, we'll do this in our "app.js" file: app.use((req, res, next) => { if (!req.session.user) { // we put this in place because if we didn't have a session id, even logging out would be a problem because this code wasn't set up to accommodate anything other than a valid session id. Now, it just "moves along" so something like logging out doesn't become a problem return next(); } User.findById(req.session.user._id) .then(user => { req.user = user; next(); }) .catch(err => console.log(err)); }); Now, if we've got a "req.session.user," we keep moving. Otherwise, we grab the user object from the database that was put in place by our "auth.js" file. Now, if we go back to our initial problem with "shop.js" file, before we were basing our method on "req.session.user." Again, that wasn't a bad thing, but we were storing our session user differently. Now, our code will look like this (this is just one example of what we had in "store.js." We had to do this throughout the app...): exports.getCart = (req, res, next) => { req.user // this used to be "req.session.user" .populate("cart.items.productId") .execPopulate() .then(user => { // console.log(req.user); const products = user.cart.items; res.render("shop/cart", { path: "/cart", pageTitle: "Your Cart", products: products, isAuthenticated: req.session.isLoggedIn }); }) .catch(err => console.log(err)); }; The order of your elements in app.js matters! This is the real world project I was tasked with building that transformed a working PHP application into a MERN app. A) Basic Setup (back to top...) To get started, I referred to the list I've got documented here.
IMPORTANT! Remember: The order matters! I got stuck for a little bit at the very beginning in that I couldn't get my "index.js" page to show because of the code I've got referenced below. Keep that in mind going forward!
1) app.js (back to top...) app.js:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const errorController = require("./controllers/error"); const app = express(); app.set("view engine", "ejs"); app.set("views", "views"); const startRoutes = require("./routes/start"); app.use(bodyParser.urlencoded()); app.use(express.static(path.join(__dirname, "public"))); app.use(startRoutes); // this code is correct and all of the corresponding files are bulletproof. But I originally had this line in AFTER the "app.use(errorController.get404" line and, as a result, everything was routed to the error page. app.use(errorController.get404); app.listen(3001);
2) start.js (route) (back to top...) Here's your "start.js" route file:
const path = require("path"); const express = require("express"); const rootDir = require("../utility/path"); const startController = require("../controllers/start"); const authController = require("../controllers/auth"); const router = express.Router(); router.get("/", startController.getIndex); router.get("/login", authController.getLogin); module.exports = router;
The highlighted sections are what you want to pay attention to in that they're directing your basic routing to the index page and the login page which you'll qualify in the next step, as far as security is concerned. 3) start.js (controller) (back to top...) Here's your "start" controller... exports.getIndex = (req, res, next) => { res.render("index", { pageTitle: "Login", path: "/" }); }; ...and here's your "auth" controller: exports.getLogin = (req, res, next) => { res.render("login", { pageTitle: "Login", path: "/login" }); }; 4) index.ejs / login.ejs (view) (back to top...) And here's your views: index.ejs <%-include('includes/header.ejs') %> </head> <body> <%-include('includes/top.ejs') %> <%-include('includes/navigation.ejs') %> <main> Hello </main> <%-include('includes/footer.ejs') %> login.ejs
<%-include('includes/header.ejs') %> </head> <body> <%-include('includes/top.ejs') %> <%-include('includes/navigation.ejs') %> <main> Welcome to the "Landing Pages Admin Suite!" <br><br> To start, go ahead and login below by entering your email and your password! <br><br> <div style="margin:auto; width:300px; border:1px solid #ccc; border-radius:10pt; box-shadow:5px 5px 3px #ccc; padding:10px;"> <form class="login-form" action="/login" method="POST"> <table style="margin:auto;"> <tr> <td>email: </td> <td> <div class="form-control"> <input type="email" name="email" id="email"> </div> </td> </tr> <tr> <td>password: </td> <td> <div class="form-control"> <input type="password" name="password"id="password"> </div> </td> </tr> <tr> <td style="text-align:center; padding-top:10px;" colspan="2"><button class="btn" type="submit">Login</button></td> </tr> </table> </form> </div> </main> <%-include('includes/footer.ejs') %>
B) Security (back to top...) 1) Necessary Middleware and Packages Here are your necessary packages as well as the "app.js" code that imports and registers them. a) Mongoose (back to top...) npm install --save mongoose i) app.js Here's the code that will need to be in place on your "app.js" file: const mongoose = require("mongoose"); // import the package const MONGODB_URI = "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/test"; // define your connection and collection mongoose .connect(MONGODB_URI) .then(result => { app.listen(3001); }) .catch(err => { console.log(err); }); // establish your connection and then "listen" on your perscribed port b) Express Session (back to top...) Here's what we'll use to create Session data... npm install --save express-session i) app.js (back to top...) In our "app.js" file, we'll import it and register it like this:
const session = require("express-session"); -- app.use( session({ secret: "my secret", resave: false, saveUninitialized: false, store: store }) );
1
this first parameter - "secret" - is the only required paramater, although you can obviously incorporate several others. Once this in place, all the requests to the app routes are now using sessions. "secret" is the only required parameter. It should be a unique string for your application. You can read more about this by clicking here. Remember, you're using this to create a session variable. The next app will actually store it in a database. That's where
2
is coming from. c) MongoDB Session (back to top...) Now that we can create Session data, here's what we need in order to store it. npm install --save connect-mongodb-session i) app.js Here's the way it looks like in our "app.js" file...
const store = new MongoDBStore({ uri: MONGODB_URI, collection: "sessions" }); --- app.use( session({ secret: "my secret", resave: false, saveUninitialized: false, store: store }) ); --- app.use((req, res, next) => { if (!req.session.user) { return next(); } User.findById(req.session.user._id) .then(user => { req.user = user; next(); }) .catch(err => console.log(err)); });
1
click here for a reminder on what all that was about d) bcryptjs (back to top...) npm install --save bycryptjs i) auth.js (back to top...) You'll put this in your "auth.js" controller...
const bcrypt = require("bcryptjs"); exports.getLogin = (req, res, next) => { res.render("login", { pageTitle: "Login", path: "/login" }); }; exports.postLogin = (req, res, next) => { const email = req.body.email; const password = req.body.password; User.findOne({ email: email }) .then(user => { if (!user) { req.flash("error", "Invalid email or password."); return res.redirect("/login"); } bcrypt .compare(password, user.password) .then(doMatch => { if (doMatch) { req.session.isLoggedIn = true; req.session.user = user; return req.session.save(err => { console.log(err); res.redirect("/"); }); } res.redirect("/login"); }) .catch(err => { console.log(err); res.redirect("/login"); }); }) .catch(err => console.log(err)); };
Once those are in place, you want to set up your users table... A) Signup Form and Insert Code (back to top...) 1) signup.ejs (back to top...) Here's the signup.ejs page:
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/forms.css"> <link rel="stylesheet" href="/css/auth.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <form class="login-form" action="/signup" method="POST"> <div class="form-control"> <label for="email">E-Mail</label> <input type="email" name="email" id="email"> </div> <div class="form-control"> <label for="password">Password</label> <input type="password" name="password" id="password"> </div> <div class="form-control"> <label for="confirmPassword">Confirm Password</label> <input type="password" name="confirmPassword" id="confirmPassword"> </div> <button class="btn" type="submit">Signup</button> </form> </main> <%- include('../includes/end.ejs') %>
2) auth.js (Controller) (back to top...) Here's your Controller:
exports.postSignup = (req, res, next) => { const email = req.body.email; const password = req.body.password; const confirmPassword = req.body.confirmPassword; User.findOne({ email: email }) .then(userDoc => { if (userDoc) { return res.redirect("/signup"); } const user = new User({ email: email, password: password, cart: { items: [] } }); return user.save(); console.log("user saved"); }) .then(result => { res.redirect("/login"); }) .catch(err => { console.log(err); }); };
1
notice the convention of the code. By that I mean that the "ejs" file has the form action as "/signup." In order to propertly connect the dots, your Controller has to be set up as "postSignup" in order for the code to be triggered... And that's because your route looks like this: router.post('/signup', authController.postSignup); Nothing complicated, but something to be aware of nevertheless. FYI: We changed the schema of the "users" table by eliminating the "name" column. B) Encryption / bcryptjs (back to top...) To encrypt the password, start by importing the "bcryptjs" package:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ npm install --save bcryptjs
Once that's in place, here's how your "auth.js" Controller is going to look:
const bcrypt = require("bcryptjs"); ... exports.postSignup = (req, res, next) => { const email = req.body.email; const password = req.body.password; const confirmPassword = req.body.confirmPassword; User.findOne({ email: email }) .then(userDoc => { if (userDoc) { return res.redirect("/signup"); } return bcrypt .hash(password, 12) .then(hashedPassword => { const user = new User({ email: email, password: hashedPassword, cart: { items: [] } }); return user.save(); }) .then(result => { res.redirect("/login"); }); }) .catch(err => { console.log(err); }); };
1
import the bcryptjs package
2
encrypt the incoming password from your form
3
insert the newly encrypted password along with your other user data C) auth.js Controller (back to top...) Here's your login Controller (auth.js)...
exports.postLogin = (req, res, next) => { const email = req.body.email; const password = req.body.password; User.findOne({ email: email }) .then(user => { if (!user) { return res.redirect("/login"); } bcrypt .compare(password, user.password) .then(doMatch => { if (doMatch) { req.session.isLoggedIn = true; req.session.user = user; return req.session.save(err => { console.log(err); res.redirect("/"); }); } res.redirect("/login"); }) .catch(err => { console.log(err); res.redirect("/login"); }); }) .catch(err => console.log(err)); };
2
you had to have this "return" in place, otherwise the code just kept moving down the line and you're redirected to the login page and nothing really happens :( D) Route Protection (back to top...) 1) Using a Line by Line Approach (back to top...) If you wanted to protect a route, you could do it like this:
exports.getAddProduct = (req, res, next) => { if (!req.session.isLoggedIn) { return res.redirect("/login"); } res.render("admin/edit-product", { pageTitle: "Add Product", path: "/admin/add-product", editing: false, isAuthenticated: req.session.isLoggedIn }); };
Pretty straight forward. But if you wanted to do it this way, you would have to write this code with every route you wanted to protect. You can do it a more scaleable way using Middleware... 2) Using Middleware (back to top...) Start by creating a "middleware" folder and then adding this as an "is-auth.js" file... module.exports = (req, res, next) => { if (!req.session.isLoggedIn) { return res.redirect("/login"); } next(); }; Now, you're going to import that into your "admin" and your "shop" route pages and it will look like this:
admin.js route... const path = require("path"); const express = require("express"); const adminController = require("../controllers/admin"); const isAuth = require("../middleware/is-auth"); const router = express.Router(); // /admin/add-product => GET router.get("/add-product", isAuth, adminController.getAddProduct); // /admin/products => GET router.get("/products", isAuth, adminController.getProducts); // /admin/add-product => POST router.post("/add-product", isAuth, adminController.postAddProduct); router.get("/edit-product/:productId", isAuth, adminController.getEditProduct); router.post("/edit-product", isAuth, adminController.postEditProduct); router.post("/delete-product", isAuth, adminController.postDeleteProduct); module.exports = router;
E) Understanding and Preventing CSRF Attacks (back to top...) CSRF stands for "Cross Site Request Forgery." Basically, someone makes a site that looks just like yours. They think they're interacting with your site and, in a way, they are, as far as the backend is concerned. But the frontend has them doing things they wouldn't normally do and the result can be disasterous. To prevent that from happening, we're going to use a package called, "csurf." Start by installing that...
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ npm install --save csurf
Once that's in place, you'll need to import that package using your "app.js" file. Also, you're going to incorporate something called "locals." This gives you the ability to declare your "authenticated" and your "token" variables on a global scale.
const csrf = require("csurf"); ... const csrfProtection = csrf(); ... app.use(csrfProtection); ... app.use((req, res, next) => { res.locals.isAuthenticated = req.session.isLoggedIn; res.locals.csrfToken = req.csrfToken(); next(); });
Once that's in place, you'll have to go back through your site and add a hidden field to any form dynamic where a CSRF attack would be possible. For example, your "logout" button... <li class="main-header__item"> <form action="/logout" method="post"> <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <button type="submit">Logout</button> </form> </li> F) Providing User Feedback (back to top...) Right now, when a user fails to login successfully, they're redirected to a login page or an index page, but there's no feedback. To solve that we're going to use a package called, "connect-flash." 1) Import / Register it in "app.js" (back to top...)
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ npm install --save connect-flash
With that installed, we now import it into our "app.js" file by first storing it in a constant: const flash = require("connect-flash"); ...and then registering it AFTER the session has been defined: app.use( session({ secret: "my secret", resave: false, saveUninitialized: false, store: store }) );// after session... app.use(csrfProtection); app.use(flash()); 2) Add it to postLogin on "auth.js" Controller (back to top...) Once that's in place, we're now looking at the "auth.js" Controller:
exports.postLogin = (req, res, next) => { const email = req.body.email; const password = req.body.password; User.findOne({ email: email }) .then(user => { if (!user) { req.flash("error", "Invalid email or password."); return res.redirect("/login"); } bcrypt .compare(password, user.password) .then(doMatch => { if (doMatch) { req.session.isLoggedIn = true; req.session.user = user; return req.session.save(err => { console.log(err); res.redirect("/"); }); } res.redirect("/login"); }) .catch(err => { console.log(err); res.redirect("/login"); }); }) .catch(err => console.log(err)); };
3) Add it to the "login.ejs" View (back to top...)
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/auth.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <% if(errorMessage) { %> <div class="user-message user-message---error"><%= errorMessage %></div> <% } %> <br> <form action='/login' method="Post"> <table style="width:300px; margin:auto; border:1px solid #ccc; border-radius:10pt; padding:10px; box-shadow:5px 5px 3px #ccc;"> <tr> <td><span style="font-size:11pt;">Email</span></td> <td><div class="form-control"></div><input type="email" name="email" id="email"></div></td> </tr> <tr> <td><span style="font-size:11pt;">Password</span></td> <td><div class="form-control"></div><input type="password" name="password" id="password"></div></td> </tr> <tr> <td colspan="2" style="text-align:center;"> <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <button class="btn" type="submit">Login</button> </td> </tr> </table> </form> </main> <%- include('../includes/end.ejs') %>
A) sendgrid.com (back to top...) You'll start by setting up a free account with sendgrid.com and then installing their package into your app like this:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ npm install --save nodemailer nodemailer-sendgrid-transport
B) auth.js (back to top...) Here's the code:
const bcrypt = require("bcryptjs"); const nodemailer = require("nodemailer"); const sendgridTransport = require("nodemailer-sendgrid-transport"); const User = require("../models/user"); const transporter = nodemailer.createTransport( sendgridTransport({ auth: { api_key: "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" } }) ); ... exports.postSignup = (req, res, next) => { const email = req.body.email; const password = req.body.password; const confirmPassword = req.body.confirmPassword; User.findOne({ email: email }) .then(userDoc => { if (userDoc) { req.flash("error", "email already exists...!"); return res.redirect("/signup"); } return bcrypt .hash(password, 12) .then(hashedPassword => { const user = new User({ email: email, password: hashedPassword, cart: { items: [] } }); return user.save(); }) .then(result => { console.log(email); res.redirect("/login"); return transporter.sendMail({ to: email, from: "bruce@muscularchristianityonline.com", subject: "Thanks for logging in!", html: "

You successfully signed up!

" }); }) .catch(err => { console.log(err); }); }) .catch(err => { console.log(err); }); };
1
import nodemailer
2
import sendgridTransport
3
document your sendgridTransport credentials
4
redirect to your login page first before you run your email script so you don't slow things down
5
your actual email script BTW: The order of
4
and
5
is significant because if you're sending out a bunch of emails, it will slow the process down. The way things are set up here is sound. A) Resetting Passwords (back to top...) 1) reset-password.ejs (back to top...)
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/auth.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <% if(errorMessage) { %> <div class="user-message user-message---error"><%= errorMessage %></div> <% } %> <br> <form action='/reset' method="Post"> <table style="width:300px; margin:auto; border:1px solid #ccc; border-radius:10pt; padding:10px; box-shadow:5px 5px 3px #ccc;"> <tr> <td><span style="font-size:11pt;">Email</span></td> <td><div class="form-control"><input type="email" name="email" id="email"></div></td> </tr> <tr> <td colspan="2" style="text-align:center;"><br> <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <button class="btn" type="submit">Reset Password</button> </td> </tr> </table> </form> </main> <%- include('../includes/end.ejs') %>
Most of this is just a "copy and paste" from you login page with the email field omitted.
1
this is important, obviously, and we'll come back to that in a minute 2) getReset Controller (back to top...) 3) postReset Controller (back to top...) Here's your Controller: exports.getReset = (req, res, next) => { let message = req.flash("error"); if (message.length > 0) { message = message[0]; } else { message = null; } res.render("auth/reset", { path: "/reset", pageTitle: "Reset Password", errorMessage: message }); }; Cake and ice cream...
exports.postReset = (req, res, next) => { crypto.randomBytes(32, (err, buffer) => { if (err) { console.log(err); return res.redirect("/reset"); } const token = buffer.toString("hex"); User.findOne({ email: req.body.email }) .then(user => { if (!user) { req.flash("error", "Sorry! That email wasn't found."); return res.redirect("/reset"); } user.resetToken = token; user.resetTokenExpiration = Date.now() + 360000; return user.save(); }) .then(result => { res.redirect("/"); transporter.sendMail({ to: req.body.email, from: "bruce@muscularchristianityonline.com", subject: "Password Reset!", html: ` <p>You requested a password reset.</p> <p>Click this <a href="localhost:3000/reset/${token}">link</a>>to set a new password. ` }); }) .catch(err => { console.log(err); }); }); };
1
"crypto" is a library within Node.js that allows you to create uniqe and secure random values. "32" is the number of bytes. Provided everyting is working smoothly, you get a "buffer..." What's a buffer? JavaScript is a unicode friendly environment. In other words, it processes every letter in the alphabet as a number. Binary data is just one's and zeroes. Ultimately, that's how a computer processes every command, but the two don't really play well together (click here to see how a computer converts text into Binary code). While you can get into some serious detail when it comes to the definition of a "buffer," the bottom line is that you're creating a digital space for binary data that would otherwise cause your JavaScript app to blow a fuse. Transmission Control Protocol (TCP), the way data is being moved back and forth on the internet, including octet streams (attachments are usually octet-streams) need to be handled differently by JavaScript because of their Binary basis. You can read more about it here. So...you need a buffer and that's what this is.
2
Provided it gets set up properly, you're going to take a binary value that is 32 bytes and convert that to a string using a hexadecimal dynamic.
3
find your user according to the email address that the user entered
4
you're adding a resetToken and a resetTokenExpiration value to the document so as to ensure a timely and secure update
5
sending an email to the user that will include a link connecting them to a page where they can reset their password 4) getNewPassword Controller (back to top...) a) reset password link / route (back to top...) This is the link / text that was sent to the user via email: Click this <a href="localhost:3000/reset/${token}">link</a>>to set a new password. The token was the random number we generated using "crypto." So, here's the route that will accommodate that link: router.get("/reset/:token", authController.getNewPassword); Notice the "/" character before your parameter. That's crucial... b) the Controller (back to top...) Here's your Controller. Notice how it's grabbing the param from your URL...
exports.getNewPassword = (req, res, next) => { const token = req.params.token; User.findOne({ resetToken: token, resetTokenExpiration: { $gt: Date.now() } }) .then(user => { let message = req.flash("error"); if (message.length > 0) { message = message[0]; } else { message = null; } console.log(user); res.render("auth/new-password", { path: "/new-password", pageTitle: "New Password", errorMessage: message, userId: user._id.toString(), passwordToken: token }); }) .catch(err => { console.log(err); }); };
1
req.params.token - "token" is what allows the system to match that with router.get("/reset/:token", authController.getNewPassword);
2
pass the userId into your view...
3
pass the passwordToken (which was stored in the database at the very beginning of this process) into your view c) the reset-password view (back to top...) Here's the page that the user gets when they click on the email that they've received thanks to the Controller that we just covered.
<%- include('../includes/head.ejs') %> <link rel="stylesheet" href="/css/auth.css"> </head> <body> <%- include('../includes/navigation.ejs') %> <main> <% if(errorMessage) { %> <div class="user-message user-message---error"><%= errorMessage %></div> <% } %> <br> <form action='/new-password' method="Post"> <table style="width:300px; margin:auto; border:1px solid #ccc; border-radius:10pt; padding:10px; box-shadow:5px 5px 3px #ccc;"> <tr> <td><span style="font-size:11pt;">Password</span></td> <td><div class="form-control"><input type="password" name="password" id="password"></div></td> </tr> <tr> <td colspan="2" style="text-align:center;"><br> <input type="hidden" name="userId" value="<%= userId %>" /> <input type="hidden" name="passwordToken" value="<%= passwordToken %>"> <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <button class="btn" type="submit">Update Password</button> </td> </tr> </table> </form> </main> <%- include('../includes/end.ejs') %>
5) postNewPassword Controller
exports.postNewPassword = (req, res, next) => { const newPassword = req.body.password; const userId = req.body.userId; const passwordToken = req.body.passwordToken; let resetUser; User.findOne({ resetToken: passwordToken, resetTokenExpiration: { $gt: Date.now() }, _id: userId }) .then(user => { resetUser = user; return bcrypt.hash(newPassword, 12); }) .then(hashedPassword => { resetUser.password = hashedPassword; resetUser.resetToken = undefined; resetUser.resetTokenExpiration = undefined; return resetUser.save(); }) .then(result => { res.redirect("/login"); }) .catch(err => { console.log(err); }); };
1
temporary variable that you'll use to house the current user
2
you've found the current user in the database based on the incoming password token and assuming that it hasn't expired and the incoming userId matches as well, we're going to store the matching user in the "resetUser" variable and create an encrypted version of the new password
3
save everything and reset the token and the tokenExpiration value to undefined B) Adding Authorization (back to top...) 1) Displaying Products (back to top...) To ensure that only the user who added a product is able to delete it or edit it, you want to limit the products that are being displayed in your "getProducts" method on your "admin.js" Controller like this: exports.getProducts = (req, res, next) => { Product.find({ userId: req.user._id }) .then(products => { console.log(products); res.render("admin/products", { prods: products, pageTitle: "Admin Products", path: "/admin/products", isAuthenticated: req.session.isLoggedIn }); }) .catch(err => console.log(err)); }; 2) Editing Products (back to top...)
postEditProduct.js (before)
  1. exports.postEditProduct = (req, res, next) => {
  2. const prodId = req.body.productId;
  3. const updatedTitle = req.body.title;
  4. const updatedPrice = req.body.price;
  5. const updatedImageUrl = req.body.imageUrl;
  6. const updatedDesc = req.body.description;
  7. Product.findById(prodId)
  8. .then(product => {
  9. if(product.userId !== req.user._id) {
  10. return res.redirect('/');
  11. }
  12. product.title = updatedTitle;
  13. product.price = updatedPrice;
  14. product.description = updatedDesc;
  15. product.imageUrl = updatedImageUrl;
  16. return product.save();
  17. })
  18. .then(result => {
  19. console.log("UPDATED PRODUCT!");
  20. res.redirect("/admin/products");
  21. })
  22. .catch(err => console.log(err));
  23. };
postEditProduct (after)
  1. exports.postEditProduct = (req, res, next) => {
  2. const prodId = req.body.productId;
  3. const updatedTitle = req.body.title;
  4. const updatedPrice = req.body.price;
  5. const updatedImageUrl = req.body.imageUrl;
  6. const updatedDesc = req.body.description;
  7. Product.findById(prodId)
  8. .then(product => {
  9. if (product.userId.toString() !== req.user._id.toString()) {
  10. return res.redirect('/');
  11. }
  12. product.title = updatedTitle;
  13. product.price = updatedPrice;
  14. product.description = updatedDesc;
  15. product.imageUrl = updatedImageUrl;
  16. return product.save()
  17. .then(result => {
  18. console.log("UPDATED PRODUCT!");
  19. res.redirect("/admin/products");
  20. })
  21. })
  22. .catch(err => console.log(err));
  23. };
Notice on line #13 how the ".then" block is separate from the first ".then" block? This is going to be a problem because if the user._id field does NOT match, then the user is going to get re-routed to the "index" page, correct? Wrong! This goes back to the basics, somewhat, but it's worth repeating. JavaScript is a ">Single Thread dynamic which means that we start at the top and we keep going till we're done. We don't pause or wait for anything to finish. "Callbacks" allow us the opportunity to tweak that dynamic a little bit so we can circumvent JavaScript's otherwise strict approach to code execution. The "Promise" is part of that "callback" dynamic in that we can place ".then" blocks and dictate how code is going to flow based on the successful completion of a particular task. But you need to be aware that those ".then" blocks have a bit of a liability attached to them. Take a look at the diagram below:
Every ".then" block is going to be executed, regardless if you have a "return" within that particular ".then" configuration. JS will go through every one of the ".then" blocks before it jumps back into the Call Stack. That's why we had to move the last ".then" block from here: exports.postEditProduct = (req, res, next) => { const prodId = req.body.productId; const updatedTitle = req.body.title; const updatedPrice = req.body.price; const updatedImageUrl = req.body.imageUrl; const updatedDesc = req.body.description; Product.findById(prodId) .then(product => { if(product.userId !== req.user._id) { return res.redirect('/'); } product.title = updatedTitle; product.price = updatedPrice; product.description = updatedDesc; product.imageUrl = updatedImageUrl; return product.save(); }) .then(result => { console.log("UPDATED PRODUCT!"); res.redirect("/admin/products"); }) .catch(err => console.log(err)); }; ...to here: exports.postEditProduct = (req, res, next) => { const prodId = req.body.productId; const updatedTitle = req.body.title; const updatedPrice = req.body.price; const updatedImageUrl = req.body.imageUrl; const updatedDesc = req.body.description; Product.findById(prodId) .then(product => { if (product.userId.toString() !== req.user._id.toString()) { return res.redirect('/'); } product.title = updatedTitle; product.price = updatedPrice; product.description = updatedDesc; product.imageUrl = updatedImageUrl; return product.save() .then(result => { console.log("UPDATED PRODUCT!"); res.redirect("/admin/products"); }) }) .catch(err => console.log(err)); }; It is now "nested" in the first ".then" block so the "return" will ne honored. Now, even if a user would be able to somehow access a product, if they try to edit it, they'll be redirected and the changes won't be saved. BTW:
1
BTW: you need to add ".toString()" in order to accommodate the fact that you're looking for both value and datatype. 3) Deleting Products (back to top...) This is pretty easy. You're just changing your "FindOne" query into something that's looking for things based on the productId as well as the user._id field. exports.postDeleteProduct = (req, res, next) => { const prodId = req.body.productId; //Product.findByIdAndRemove(prodId) Product.deleteOne({ _id: prodId, usesrId: req.user._id }) .then(() => { console.log("DESTROYED PRODUCT"); res.redirect("/admin/products"); }) .catch(err => console.log(err)); }; A) Basic Email Validation (back to top...) Start by installing express-validator with this command:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ npm install --save express-validator
1) auth.js Routes (back to top...) This is the "basic" version where you're just going to get a system generated message. It's good though because it will alert the user of a bad email if they didn't use the "@" sign. No, once you have installed the "express-validator" package, you're going to import it on your "auth.js" routes page. const { check } = require("express-validator/check"); Notice how you're only importing a sub-package from the "express-validator" package. Here you're using a technique called "Destructuring" which is a shorthand way of storing variables from within an array. After importing that package, you'll do this on your actual, "post signup" route. router.post("/signup", check("email").isEmail(), authController.postSignup); You're using the middleware represented by the "check" variable to see if what the user entered is a valid email address. 2) auth.js Controller (back to top...) import the "validationResult" object that's available from the "express-validator/express" package const { validationResult } = require("express-validator/check"); ..and then for your actual "postSignup" method, you'll do this:
exports.postSignup = (req, res, next) => { const email = req.body.email; const password = req.body.password; const confirmPassword = req.body.confirmPassword; const errors = validationResult(req); if (!errors.isEmpty()) { console.log(errors.array()); return res.status(422).render("auth/signup", { path: "/signup", pageTitle: "Signup", errorMessage: errors.array() }); }
...and that will get you this result when you type in a bad email...
BTW: The above message is not a result of any of the validation middleware you have installed. Rather, it's Chrome's way of alerting the user that they've entered a bogus value and that comes from the fact that the input type was set to "email!"
1
storing the validationResults in the "errors" constant
2
if you've got some errors, you're going to render the "signup" view with a 422 status
3
this is the errorMessage that you had originally used as part of your "flash" dynamic. We're going to use that in a minute... B) Using the Error Message Field (back to top...) When you look at your terminal to see the actual error that is being generated from your "express-validator" package, you see this:
[ { value: '', msg: 'Invalid value', param: 'email', location: 'body' } ]
You can disable any kind of validation by adding "novalidation" to your form (<form action='/new-password' method="Post" novalidate>
You're getting in this array the value that was entered, the parameter (which matches the "name" of the field" and the location of the actual field itself. The thing we want to harness is the "msg." Here's why: If the user doesn't add ANY email address, they won't get a message that reminds them they need to add an "@" sign, instead they'll get the errors.array() which is currently assigned to the errorMessage variable.
The problem is, it's an array and we need to be more specific. Right now, the error looks like what you see to the right. To remedy that, we're going to change
3
above to this: errorMessage: errors.array()[0].msg ...and now we get this (Invalid Email): Now, if we wanted to customize that message so we weren't limited to just the default message
coming from the "express-validation" package, we could do this: router.post( "/signup", check("email") .isEmail() .withMessage("Please enter a valid email."), authController.postSignup ); When you do that, the user will see this:
C) Custom Validator Fields (back to top...) You can create your own custom validation dynamics! Take a look at what's below:
router.post( "/signup", check("email") .isEmail() .withMessage("Please enter a valid email.") .custom((value, { req }) => { if (value === "test@test.com") { throw new Error("This email address is forbidden!"); } return true; }), authController.postSignup );
You're just going to follow the format that you see highlighted above and that's all there is to it. Be sure to return "true" at the end as that will allow the code to continue.
BTW: "express-validator" includes the "validator.js" library. You'll see "express-validator" described as something that "wraps" around "validator.js." It can be somewhat confusing in that there is a "wrap" function within JQuery. In this context, however, it's referring to the way in which "express-validator" is built on top of "validator.js."
D) More Validators (back to top...) Here we're looking to see if the password is long enough and there aren't anything other than alphanumeric characters.
const { check, body } = require("express-validator/check"); router.post( "/signup", [ check("email") .isEmail() .withMessage("Please enter a valid email.") .custom((value, { req }) => { if (value === "test@test.com") { throw new Error("This email address is forbidden!"); } return true; }), body( "password", "Please enter a password that consists of at least 5 characters and contains only alphanumeric characters!" ) .isLength({ min: 5 }) .isAlphanumeric() ], authController.postSignup );
1
bringing into the mix another aspect of the "express-validator" package and that is the "body" dynamic. This allows you to look for and identify criteria within the body of the document. This is where we're going to find the password.
2
&
3
looking for the password within the body of the document that we're evaluating (as opposed to the header...)
4
this is a great way of defining an error message for all of the checks you're getting ready to do against the password field
5
checking for the length of the password and you're using a JQuery object as part of your evaluation. BTW: A JQuery Object is something you've been working with, but weren't necessarily aware of its name when you were looking at it. Here's the way the JavaScript Object is defined on W3Schools... var person = {firstName:"John", lastName:"Doe", age:50, eyeColor:"blue"}; The content between the curly braces is your Object. Here's some more info from smashingmagazine.com
And that's how you do it! Now you've got validation in place for your password that looks for the length and the type of characters that have been entered. E) Equality (back to top...) To check for form field equality, we'll do something like this:
router.post( "/signup", [ check("email") .isEmail() .withMessage("Please enter a valid email.") .custom((value, { req }) => { if (value === "test@test.com") { throw new Error("This email address is forbidden!"); } return true; }), body( "password", "Please enter a password that consists of at least 5 characters and contains only alphanumeric characters!" ) .isLength({ min: 5 }) .isAlphanumeric(), body("confirmPassword").custom((value, { req }) => { if (value !== req.body.password) { throw new Error("Passwords have to match!"); } return true; }) ], authController.postSignup );
Pretty straight forward! F) Async Validation (back to top...) To make this happen, you're going to incorporate a "Promise Reject" dynamic and it's going to look like this:
router.post( "/signup", [ check("email") .isEmail() .withMessage("Please enter a valid email.") .custom((value, { req }) => { // if (value === "test@test.com") { // throw new Error("This email address is forbidden!"); // } // return true; return User.findOne({ email: value }).then(userDoc => { if (userDoc) { return Promise.reject( "Email already exists, please pick a different one." ); } }); }), body( "password", "Please enter a password that consists of at least 5 characters and contains only alphanumeric characters!" ) .isLength({ min: 5 }) .isAlphanumeric(), body("confirmPassword").custom((value, { req }) => { if (value !== req.body.password) { throw new Error("Passwords have to match!"); } return true; }) ], authController.postSignup );
Now that you're doing this on the "route" side of the picture, you're not going to use what you had in your "route.js" Controller, so...
auth.js Controller (before)
  1. exports.postSignup = (req, res, next) => {
  2. const email = req.body.email;
  3. const password = req.body.password;
  4. const errors = validationResult(req);
  5. if (!errors.isEmpty()) {
  6. console.log(errors.array());
  7. return res.status(422).render("auth/signup", {
  8. path: "/signup",
  9. pageTitle: "Signup",
  10. errorMessage: errors.array()[0].msg
  11. });
  12. }
  13. User.findOne({ email: email })
  14. .then(userDoc => {
  15. if (userDoc) {
  16. req.flash("error", "email already exists...!");
  17. return res.redirect("/signup");
  18. }
  19. return bcrypt
  20. .hash(password, 12)
  21. .then(hashedPassword => {
  22. const user = new User({
  23. email: email,
  24. password: hashedPassword,
  25. cart: { items: [] }
  26. });
  27. return user.save();
  28. })
  29. .then(result => {
  30. console.log(email);
  31. res.redirect("/login");
  32. return transporter.sendMail({
  33. to: email,
  34. from: "bruce@muscularchristianityonline.com",
  35. subject: "Thanks for logging in!",
  36. html: "<h1>You successfully signed up!</h1>"
  37. });
  38. })
  39. .catch(err => {
  40. console.log(err);
  41. });
  42. })
  43. .catch(err => {
  44. console.log(err);
  45. });
  46. };
auth.js Controller (after)
  1. exports.postSignup = (req, res, next) => {
  2. const email = req.body.email;
  3. const password = req.body.password;
  4. const errors = validationResult(req);
  5. if (!errors.isEmpty()) {
  6. console.log(errors.array());
  7. return res.status(422).render("auth/signup", {
  8. path: "/signup",
  9. pageTitle: "Signup",
  10. errorMessage: errors.array()[0].msg
  11. });
  12. }
  13. bcrypt
  14. .hash(password, 12)
  15. .then(hashedPassword => {
  16. const user = new User({
  17. email: email,
  18. password: hashedPassword,
  19. cart: { items: [] }
  20. });
  21. return user.save();
  22. })
  23. .then(result => {
  24. console.log(email);
  25. res.redirect("/login");
  26. return transporter.sendMail({
  27. to: email,
  28. from: "bruce@muscularchristianityonline.com",
  29. subject: "Thanks for logging in!",
  30. html: "<h1>You successfully signed up!</h1>"
  31. });
  32. })
  33. .catch(err => {
  34. console.log(err);
  35. });
  36. };
You got rid of your "User.findOne()" function and you launched immediately into your "bcrypt" function. After that, you just got rid of the extra curly braces etc. And there you go! F) Keep User Input (back to top...) Should a user attempt to login and they're told that their email doesn't match or any kind of error for that matter, this functionality preserves the info they originally inputted so they don't have to start all over again. 1) signup.ejs (back to top...) Here's the code you're using with your "ejs" file. Basically you're just adding the value tag with the EJS syntax that grabs the "oldInput" stuff.
<tr> <td><span style="font-size:11pt;">Email</span></td> <td><input type="email" name="email" id="email" value="<%=oldInput.email %>"></td> </tr> <tr> <td><span style="font-size:11pt;">Password</span></td> <td><input type="password" name="password" id="password" value="<%=oldInput.password %>"></td>> </tr> <tr> <td><span style="font-size:11pt;">Confirm Password</span></td> <td><input type="password" name="confirmPassword" id="confirmPassword" value="<%=oldInput.confirmPassword %>"></td> </tr> <tr> <td colspan="2" style="text-align:center;"><br> <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <button class="btn" type="submit">Signup</button> </td> </tr>
2) auth.js Controller (back to top...)
exports.getSignup = (req, res, next) => { let message = req.flash("error"); if (message.length > 0) { message = message[0]; } else { message = null; } res.render("auth/signup", { path: "/signup", pageTitle: "Signup", errorMessage: message, oldInput: { email: "", password: "", confirmPassword: "" } }); };
H) Conditional CSS Classes (back to top...) 1) auth.js Controller (back to top...) First thing you're going to do when you want to enhance what we just did, as far as preserving the data that the user just entered although something went south, is to add the "validationErrors" variable that you see highlighted below.
exports.postSignup = (req, res, next) => { const email = req.body.email; const password = req.body.password; const errors = validationResult(req); if (!errors.isEmpty()) { console.log(errors.array()); return res.status(422).render("auth/signup", { path: "/signup", pageTitle: "Signup", errorMessage: errors.array()[0].msg, oldInput: { email: email, password: password, confirmPassword: req.body.confirmPassword }, validationErrors: errors.array() }); } bcrypt .hash(password, 12) .then(hashedPassword => { const user = new User({ email: email, password: hashedPassword, cart: { items: [] } }); return user.save(); }) .then(result => { console.log(email); res.redirect("/login"); return transporter.sendMail({ to: email, from: "bruce@muscularchristianityonline.com", subject: "Thanks for logging in!", html: "

You successfully signed up!

" }); }) .catch(err => { console.log(err); }); };
Now, implement that into your "signup.ejs" file... 2) signup.ejs Controller (back to top...)
<form action='/signup' method="Post"> <table style="width:350px; margin:auto; border:1px solid #ccc; border-radius:10pt; padding:10px; box-shadow:5px 5px 3px #ccc;"> <tr> <td><span style="font-size:11pt;">Email</span></td> <td> <input class="<%= validationErrors.find(e=> e.param === 'email') ? 'invalid' : '' %>" type="email" name="email" id="email" value="<%=oldInput.email %>" > </td> </tr> <tr> <td><span style="font-size:11pt;">Password</span></td> <td> <input class="<%= validationErrors.find(e=> e.param === 'password') ? 'invalid' : '' %>" type="password" name="password" id="password" value="<%=oldInput.password %>" > </td> </tr> <tr> <td><span style="font-size:11pt;">Confirm Password</span></td> <td> <input class="<%= validationErrors.find(e=> e.param === 'confirmPassword') ? 'invalid' : '' %>" type="password" name="confirmPassword" id="confirmPassword" value="<%=oldInput.confirmPassword %>" > </td> </tr> <tr> <td colspan="2" style="text-align:center;"><br> <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <button class="btn" type="submit">Signup</button> </td> </tr> </table> </form>
3) forms.css (back to top...) Your "forms.css" file will look like this: .invalid { border-color: red; }
FYI: I redid the form so it shows up as a table with a box-shadow etc. We're not doing the "form-control" dynamic, so that's why you're only using ".invalid" as opposed to "form-control input.invalid."
I) Sanitizing Data (back to top...) By "sanitizing," we're referring to ensuring that the email address is valid, convert any uppercase letters to lowercase (normalizeEmail), eliminate any superflous spaces etc. You're going to do that on your router page. In this case, it will be "auth.js" and the code looks like this:
router.post( "/login", [ body("email") .isEmail() .withMessage("Please enter a valid email.") .normalizeEmail(), body("password", "Password has to be valid.") .isLength({ min: 5 }) .isAlphanumeric() .trim() ], authController.postLogin );
J) Validating Product Info (back to top...) 1) Validator Package (back to top...) While this may have been covered in the past, it's good to recap. To institute any kind of validation, you're going to need the "Express Validator" package. We're validating the user's input data as they go to enter a new product. So, we're on the "admin.js" route page. The first thing we're doing is importing the "validator" package at the top of the page: const { body } = require('express-validator/check') Again, for the sake of review, you're looking at an approach to coding called "destructuring that's basically incorporating some shorcuts that you can review by clicking here. Basically what you're doing here is telling to break apart or "destructure" the incoming data to whatever it is that you specify. In this instance, you're asking Node to break things down to what's represented by the "body" object. 1) Validator Package (back to top...) You're going to incorporate this code into the syntax you use to both insert and edit product info. Here's a "before / after..." Before... router.get("/edit-product/:productId", isAuth, adminController.getEditProduct); After
router.post( "/edit-product", [ body("title") .isAlphanumeric() .isLength({ min: 3 }) .trim(), body("imageURL").isURL(), body("price").isFloat(), body("description") .isLength({ min: 5, max: 400 }) .trim() ], isAuth, adminController.postEditProduct );
2) admin.js Controller (back to top...) The only other thing that I had to be concerned about was what I've got highlighted below...
if (!errors.isEmpty()) { console.log(errors); return res.status(422).render("admin/edit-product", { pageTitle: "Edit Product", path: "/admin/edit-product", editing: true, hasError: true, product: { title: updatedTitle, price: updatedPrice, imageUrl: updatedImageUrl, description: updatedDesc, _id: prodId // be sure to include this in the information you're reproducing if you publish an error }, errorMessage: errors.array()[0].msg, // grab some of the array functionality that's coming from your validator package and store it in the "errorMessage" variable that your .ejs file will be looking for validationErrors: errors.array() }); }
You've got different kinds of errors: Technical Errors, "Expected Errors" (possible retry, file can't be read etc.), Bugs and Logical Errors You've got an "error" object built into Node. This is something that is thrown by the system when there's a system error. Here's a comprehensive graphic that details the kinds of errors you're bound to encounter and how you're going to handle them...
The left side would be a "technical" error. The right side are more for custom validators etc. A) Error Theory (back to top...) 1) throw (back to top...) Here's the definition of "throw" from w3 schools... The throw statement throws (generates) an error. When an error occurs, JavaScript will normally stop, and generate an error message. The technical term for this is: JavaScript will throw an error. The throw statement allows you to create a custom error. The technical term for this is: throw an exception. The exception can be a JavaScript String, a Number, a Boolean or an Object. Here's an example:
const sum = (a, b) => { if (a && b) { return a + b; } throw new Error("Invalid arguments"); }; console.log(sum(1));
The "throw new Error('Invalid argments')" will look like this in your console:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ node errors-playground.js C:\wamp\www\adm\node\express_tutorial\errors-playground.js:5 throw new Error("Invalid arguments"); // this is the actual location of the error when it was "thrown" ^ Error: Invalid arguments // here's your Call Stack and a summary of everything that happened right up to the point of the error being thrown at sum (C:\wamp\www\adm\node\express_tutorial\errors-playground.js:5:9) at Object.<anonymous> (C:\wamp\www\adm\node\express_tutorial\errors-playgrou nd.js:8:13) at Module._compile (internal/modules/cjs/loader.js:688:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10) at Module.load (internal/modules/cjs/loader.js:598:32) at tryModuleLoad (internal/modules/cjs/loader.js:537:12) at Function.Module._load (internal/modules/cjs/loader.js:529:3) at Function.Module.runMain (internal/modules/cjs/loader.js:741:12) at startup (internal/bootstrap/node.js:285:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
The thing you want to take note of is that when an error is "thrown," your app crashes and burns. 2) try /catch (syncronous) If your code is being executed syncronously (meaning line by line - it's not waiting for anything like a file being uploaded or a row in a database being retrieved), you can use the "try / catch" approach. Check it out! Here's your code:
const sum = (a, b) => { if (a && b) { return a + b; } throw new Error("Invalid arguments"); }; try { console.log(sum(1)); } catch (error) { console.log("Error occured!"); console.log(error); }
When you run this, you'll get the following:
brucegust@BRUCEGUST59AC MINGW64 /c/wamp/www/adm/node/express_tutorial $ node errors-playground.js Error occured! Error: Invalid arguments at sum (C:\wamp\www\adm\node\express_tutorial\errors-playground.js:5:9) at Object. (C:\wamp\www\adm\node\express_tutorial\errors-playgrou nd.js:9:15) at Module._compile (internal/modules/cjs/loader.js:688:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10) at Module.load (internal/modules/cjs/loader.js:598:32) at tryModuleLoad (internal/modules/cjs/loader.js:537:12) at Function.Module._load (internal/modules/cjs/loader.js:529:3) at Function.Module.runMain (internal/modules/cjs/loader.js:741:12) at startup (internal/bootstrap/node.js:285:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
You get the error we put in place in the context of the "try / catch" dynamic and then the other stuff we saw earlier. The thing to bear in mind is that we can proceed if we want to simply by removing the "thrown" error. If you get rid of the throw new Error("Invalid arguments");, the app would continue to run. That's something to keep in mind when you're wanting to maintain an elegant flow to your code even if something goes south. For the asyncronous dynamic, you do what we've been doing throughout our app as far as the "then" block. Know that the "catch" portion of the "then" block is going to render all of the errors specified in your "then" clause. B) Throwing Errors in Code (app.js) (back to top...) Here's what we did to a portion of the app.js file. First here's the "before..."
app.use((req, res, next) => { if (!req.session.user) { return next(); } User.findById(req.session.user._id) .then(user => { req.user = user; next(); }) .catch(err => console.log(err)); });
Now, here's what we did to enhance it:
app.use((req, res, next) => { if (!req.session.user) { return next(); } User.findById(req.session.user._id) .then(user => { if (!user) { return next(); } req.user = user; next(); }) .catch(err => { throw new Error(err); }); });
By the way... next() just means to proceed to the "next" block of code. Sometimes you see it coded as "next" like you see in
2
, other times you see it written as return next(); like in
1
. When you use"return," you're typically "returning" a value from whatever function you're working with. In this context, however, it's a little different in that you're using "return" with middleware. Here you're telling your code to jump to the next piece of middleware (app.use...). In this case, we've got a return next(); in the first piece of middleware. It's asking if we've got a session user in place. If we don't, we're moving to the next piece of middleware. We're not going to look for a user. If you didn't code this using "return," your code would simply move to the next piece of middleware BUT it would try to execute the next piece of code in your current middleware and it would crash. With next(); you're going to the next piece of middleware or whatever represents the next piece of code in your stack. Without next();, your execution would stop at that point and you would be stuck. C) Returning Error Pages (back to top...) So, we're going to go to your admin page and temporarily import the mongoose package and use that to cause an intentional error when we attempt to add a product....
const mongoose = require("mongoose"); ... const product = new Product({ _id: new mongoose.Types.ObjectId("5d136c27d145ed475c184520"), title: title, price: price, description: description, imageUrl: imageUrl, userId: req.user });
We're going to use the feature from the newly imported Mongoose package to establish an ID that's already in the database. That's going to throw an error!
at process._tickCallback (internal/process/next_tick.js:61:11) driver: true, name: 'MongoError', index: 0, code: 11000, errmsg: 'E11000 duplicate key error collection: test.products index: _id_ dup key: { : ObjectId(\'5d136c27d145ed475c184520\') }', [Symbol(mongoErrorContextSymbol)]: {} }
1) Regular Page (back to top...) So, in this instance, a good way to handle this error is to use what we had going on with the "edit-product" dynamic and we're just going to copy and paste that code where we were normally just showing an error in the console. Like this:
.catch(err => { //console.log(err); return res.status(500).render("admin/edit-product", { pageTitle: "Add Product", path: "/admin/add-product", editing: false, hasError: true, product: { title: title, imageUrl: imageUrl, price: price, description: description }, errorMessage: "Database operation failed, please try again.", validationErrors: [] }); });
2) 500 Page (back to top...) In case something goes south systemically, however, you want a legitimate 500 page. To do that you need to do the following: Create the page in your "views." You'll just copy and paste the code you used for your "404" page. Add the following to your "error.js" controller:
exports.get500 = (req, res, next) => { res.status(500).render("500", { pageTitle: "Whoops", path: "/500", isAuthenticated: req.session.isLoggedIn }); };
We added this to our "app.js" file - this is going to define our controller... app.use(errorController.get404); app.get("/500", errorController.get500); One thing to note here is the way in which we defined the route as well as the controller to use with the "/500, errorController.get500)" thingy... And then finally, throw this into your "admin.js" Controller... .catch(err => { //console.log(err); // return res.status(500).render("admin/edit-product", { // pageTitle: "Add Product", // path: "/admin/add-product", // editing: false, // hasError: true, // product: { // title: title, // imageUrl: imageUrl, // price: price, // description: description // }, // errorMessage: "Database operation failed, please try again.", // validationErrors: [] // }); res.redirect("/500"); }); D) Using Express Middleware for Errors (back to top...) You can save yourself a lot of time and code by using the Express dynamic that's built in to handle errors. You'll do it like this: In your "admin.js" file, rather than res.redirect('/500');, you'll do this: const error = new Error(err); error.httpStatusCode = 500; return next(error); When Express sees, "return next(error)," it processes that as a cue to refer to the "app.js" file where it sees this: app.use((error, req, res, next) => { res.redirect("/500"); }); Notice you've got four arguments: error, req, res, next. This is your "error handler!" The bottom portion of the app. js file looks like this:
app.get("/500", errorController.get500); app.use(errorController.get404); app.use((error, req, res, next) => { res.redirect("/500"); });
NOTICE THE ORDER! app.get('/500', errorController.get500 has to proceed your "get404" code, otherwise the code will give you the 404 error before it gets to your 500 page. I found that out the hard way! E) Using Express Middleware for Errors-> Correctly (back to top...) While the built in Express method of handling errors is very cool, you have to wield that power correctly in order for it to function correctly. For example... If you throw an error within your "app.js" file and you do so within an asynconous loop like what you have here: app.use((req, res, next) => { if (!req.session.user) { return next(); } User.findById(req.session.user._id) .then(user => { if (!user) { return next(); } req.user = user; next(); }) .catch(err => { throw new Error(err); }); }); ...you'll never reach the error handling middleware. You'll just get lost in an eternal loop. If you were to look in your console, you have this:
(node:1320) UnhandledPromiseRejectionWarning: Error: Error: dummy
If you were to throw the error in more of an async fashion like this: app.use((req, res, next) => { throw new Error(err); if (!req.session.user) { return next(); } User.findById(req.session.user._id) .then(user => { if (!user) { return next(); } req.user = user; next(); }) .catch(err => { throw new Error(err); }); }); ... you again encounter an infinite loop but not because your error is getting lost in the weeds. Instead, your code is being generated with each request. So when it goes to run the 500 page, it has to hit the initial "app.use" code which is asking for the 500 page and thus begins a circle that never concludes. To get around that, you need to do three things: First, render the 500 page right in your app;js code as opposed to redirecting it to a View...
app.use((error, req, res, next) => { //res.redirect("/500"); res.status(500).render("500", { pageTitle: "Whoops", path: "/500", isAuthenticated: req.session.isLoggedIn }); });
So, you took the code that was in your "error.js" Controller and placed it directly in your "app.js" file. The other thing you need to do is reposition your csrf Token so it fires before you actually require it in the context of your 500 page which is looking for a Session ID (I think...). The other thing you need to do is adjust the way in which you're calling your Express Error code in the way it operates in the context of a promise.
app.use((req, res, next) => { if (!req.session.user) { return next(); } User.findById(req.session.user._id) .then(user => { if (!user) { return next(); } req.user = user; next(); }) .catch(err => { //throw new Error(err); next(new Error(err)); }); });
What's in bold is what you want to do! F) Errors & Http Response Codes (back to top...) Here's a basic breakdown:You can see a full list by heading out to https:httpstatuses.com A) What Are They? (back to top...) A good example is the Udemy Course page. You've got minimal HTML. Rather, it's just a collection of divs and JavaScript code which is a browser-side scripting language. But while JavaScript is rendering the page and altering the DOM, the actual data is coming from an API - a page whose sole purpose is to churn out data but without the GUI. The bottom line is that the front end is "de-coupled" from the front end. This is the API paradigm! And it's most often facilitated by the REST dynamic which stands for "Representational State Transfer." Here your URL is triggering your functionality and we'll see how that works in just a minute. B) Data Formats and Routing (back to top...) 1) JSON (back to top...) You've got four main choices when it comes to data formats: HTML, Plain Text, XML and JSON. JSON is your best option because the machine can read it and it's fairly easy to understand from a human stanpoint as well. The other three formats vary in complexity, but it's their readability from the machine's standpoint that is key. 2) Routing
In this diagram you're seeing the way in which a route is "directing traffic." You're also seeing what represents a very common part of the RESTful API conversation and that is "API Endpoints." It's basically a fancy name for whatever action you're taking against the database. There's several options, actually. Here's a diagram that illustrates what you have access to:
C) Core Principles (back to top...) The diagram below outlines an excellente summary of the Core Principles that characterize RESTful APIs.
D) Sending Requests and Responses | CORS Errors (back to top...) app.js...
const express = require("express"); const bodyParser = require("body-parser"); const feedRoutes = require("./routes/feed"); const app = express(); app.use(bodyParser.json()); app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); next(); }); app.use("/feed", feedRoutes); app.listen(8080);
Most of this is pretty familiar, except for one thing:
1
CORS stands for "Cross Origin Resource Sharing." Without this information being put in the header, you're going to get an error should you receiving a request from a different server. You've got three different dynamics that need to be addressed: Origin, Methods and Content Type. feed.js (routes)
const express = require("express"); const feedController = require("../controllers/feed"); const router = express.Router(); //GET /feed/posts router.get("/posts", feedController.getPosts); router.post("/post", feedController.createPost); module.exports = router;
feed.js (controller)
exports.getPosts = (req, res, next) => { res.status(200).json({ posts: [{ title: "First", content: "This is the first post!" }] }); }; exports.createPost = (req, res, next) => { const title = req.body.title; const content = req.body.content; res.status(201).json({ message: "Post created successfully", post: { id: new Date().toISOString(), title: title, content: content } }); };
Using Postman, you can proof these operations first by using http:localhost:8080/feed/posts With that you'll get:
{ "title": "My first post", "content": "This is the content of my post" }
This is the content you specified in your "feed.js" controller: exports.getPosts = (req, res, next) => { res.status(200).json({ posts: [{ title: "First", content: "This is the first post!" }] }); }; You can also check your "post" dynamic by using the same URL, adjusting Postman to accommodate a "Post" rather than a "Get," and then choosing "Body -> Raw -> JSON" and entering: { "title": "My first post", "content": "This is the content of my post" } Your "feed.js" controller processes that using: exports.createPost = (req, res, next) => { const title = req.body.title; const content = req.body.content; res.status(201).json({ message: "Post created successfully", post: { id: new Date().toISOString(), title: title, content: content } }); }; ...and give you the JSON response, "Post created successfully" etc. 1) Codepen (back to top...) Using Codepen to test this puppy, you did this:
GET REQUEST
HTML:
<button id="get">Get</button> <button id="post">Post</button>
JS:
const getButton = document.getElementById('get'); const postButton = document.getElementById('post'); getButton.addEventListener('click', () => { fetch('http://localhost:8080/feed/posts') .then(res=>res.json()) .then(resData => console.log(resData)) .catch(err => console.log(err)); });
POST REQUEST
Same kind of thing with a POST request with a couple of differences in the way you structure the HTML:
postButton.addEventListener('click', () => { fetch('http://localhost:8080/feed/post', { method: 'POST', body: JSON.stringify({ title: "A codepen post", content: "Boyhowdy" }), headers: { 'Content-Type': 'application/json' } }) .then(res=>res.json()) .then(resData => console.log(resData)) .catch(err => console.log(err)); });
1
you have to convert what would otherwise be plain text into JSON so it can be detected and read correctly.
2
you have to change the content type into JSON, again so the content can be read by the application. This is a practical application involving a React UI with a real live API dynamic! A) Retrieving Posts (back to top...) 1) Feed.js (React) (back to top...) Here's the adjustment we made to the "Feed.js" file on the React side. This is part of the "loadPosts" function: fetch('http://localhost:8080/feed/posts') 2) feed.js (Node Controller) (back to top...) Here's what we did on the Node side in the "feed.js" file: exports.getPosts = (req, res, next) => { res.status(200).json({ posts: [ { _id: "1", title: "First", content: "This is the first post!", imageUrl: "images/conjuction_junction.jpg", creator: { name: "Bruce" }, createdAt: new Date() } ] }); }; B) Creating Posts (back to top...) To create a post, you're going to change up the "Feed.js" file in your React application like this (this is a portion of the "finishEditHandler" method):
fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: postData.title, content: postData.content }) }) .then(res => { if (res.status !== 200 && res.status !== 201) { throw new Error('Creating or editing a post failed!'); } return res.json(); }) .then(resData => { console.log(resData); const post = { _id: resData.post._id, title: resData.post.title, content: resData.post.content, creator: resData.post.creator, createdAt: resData.post.createdAt };
And then you've got edit the "createPost" method that you currently have in place with your API to provide the needed fields and values that React is expecting. Like this:
exports.createPost = (req, res, next) => { const title = req.body.title; const content = req.body.content; res.status(201).json({ message: "Post created successfully", post: { _id: new Date().toISOString(), title: title, content: content, creator: { name: "Bruce" }, createdAt: new Date() } }); };
C) Adding Validation (back to top...) We're going to start by adding the Express Validation package that we've worked with before, but real quick...React has some validation happening and it's worth it to look at that... 1) React Validation (back to top...) This is from "FeedEdit.js" which is in react/src/components/Feed/FeedEdit...
const POST_FORM = { title: { value: '', valid: false, touched: false, validators: [required, length({ min: 5 })] }, image: { value: '', valid: false, touched: false, validators: [required] }, content: { value: '', valid: false, touched: false, validators: [required, length({ min: 5 })] } };
2) Adding Server Side Validation (back to top...) Most of this has already been discussed, but here you go: feed.js (router)
const express = require("express"); const { body } = require("express-validator/check"); const feedController = require("../controllers/feed"); const router = express.Router(); //GET /feed/posts router.get("/posts", feedController.getPosts); //POST /feed/post router.post( "/post", [ body("title") .trim() .isLength({ min: 5 }), body("content") .trim() .isLength({ min: 5 }) ], feedController.createPost ); module.exports = router;
1
import the Express Validator package
2
here's your validator code feed.js (controller
const { validationResult } = require('express-validator/check'); ... exports.createPost = (req, res, next) => { const errors = validationResult(req); if((!errors.isEmpty()) { return res .status(422) .json({ message: "Validation failed, entered data is incorrect", errors: errors.array() }); } const title = req.body.title; const content = req.body.content; res.status(201).json({ message: "Post created successfully", post: { _id: new Date().toISOString(), title: title, content: content, content: content, creator: { name: "Bruce" }, createdAt: new Date() } }); };
1
here's where you bring in an "IF" statement to see if you've got any errors.
Be certain that your validation criteria is the same between your React and your API! Otherwise your user may not get an error that they can see on their screen, yet the code will still fail because of validation criteria that differs on the server side!
D) Adding a Database (back to top...) post.js (model)
const mongoose = require("mongoose"); const Schema = mongoose.Schema; const postSchema = new Schema( { title: { type: String, required: true }, imageUrl: { type: String, required: true }, content: { type: String, required: true }, creator: { type: Object, required: String } }, { timestamps: true } ); module.exports = mongoose.model("Post", postSchema);
app.js const bodyParser = require("body-parser"); const mongoose = require("mongoose"); ... mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/messages" ) .then(result => { app.listen(8080); }) .catch(err => console.log(err)); And here's your feed.js Controller:
const { validationResult } = require("express-validator/check"); const Post = require("../models/post"); exports.getPosts = (req, res, next) => { res.status(200).json({ posts: [ { _id: "1", title: "First", content: "This is the first post!", imageUrl: "images/conjuction_junction.jpg", creator: { name: "Bruce" }, createdAt: new Date() } ] }); }; exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ message: "Validation failed, entered data is incorrect", errors: errors.array() }); } const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: "images/conjuction_junction.jpg", creator: { name: "Bruce" } }); post .save() .then(result => { console.log(result); res.status(201).json({ message: "Post created successfully", post: result }); }) .catch(err => { console.log(err); }); };
1
import your model
2
here's your new record as it's coming in from your "body"
3
here's your "save" functionality complete with your "catch" in case something goes south E) Static Images and Error Handling (back to top...) 1) Static Images Start by importing the "path" package into app.js: const path = require("path"); And then registering it in a way where every "/images" path is processed as a static directory that constitutes an absolute path to "images." app.use("/images", express.static(path.join(__dirname, "images"))); 2) Elegant Errors First, register the Express Error Handling Middleware on your app.js file:
app.use((error, req, res, next) => { console.log(error); const status = error.statusCode; const message = error.message; res.status(status).json({ message: message }); });
Now, make it happen on your feed.js Controller page. There are two places where it's going to happen:
const { validationResult } = require("express-validator/check"); const Post = require("../models/post"); exports.getPosts = (req, res, next) => { res.status(200).json({ posts: [ { _id: "1", title: "First", content: "This is the first post!", imageUrl: "images/conjuction_junction.jpg", creator: { name: "Bruce" }, createdAt: new Date() } ] }); }; exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: "images/conjuction_junction.jpg", creator: { name: "Bruce" } }); post .save() .then(result => { console.log(result); res.status(201).json({ message: "Post created successfully", post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
1
notice how you're "throwing" an error here as opposed to...
2
the way in which you're routing things using "next(err)." To review the difference between "throw" and "next(err)," click here. F) Displaying a Single Post (back to top...) To display a single post, you'll do this: For your feed.js Route, you'll do this: router.get("/post/:postId", feedController.getPost);
One little but crucial thing: On your route, be sure to include the "/" character after "post" and before the ":" Otherwise, it won't work.
...and then for your Controller you'll do this:
exports.getPost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } console.log("good"); res.status(200).json({ message: "Post fetched.", post: post }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
Finally, on your "SinglePost.js" file located in src/pages/feed/SinglePost, you'll make this little change:
componentDidMount() { const postId = this.props.match.params.postId; fetch('http://localhost:8080/feed/post/' + postId) .then(res => { console.log("here"); if (res.status !== 200) { throw new Error('Failed to fetch status'); } return res.json(); }) .then(resData => { this.setState({ title: resData.post.title, author: resData.post.creator.name, date: new Date(resData.post.createdAt).toLocaleDateString('en-US'), content: resData.post.content }); }) .catch(err => { console.log(err); }); }
G) Uploading an Image (back to top...) A couple of things about this puppy... Here's your app.js elements. You're going to use something called, "multer."
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const mongoose = require("mongoose"); const multer = require("multer"); //const uuidv4 = require("uuid/v4"); const fileStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "images"); }, filename: (req, file, cb) => { //cb(null, uuidv4()); cb(null, file.originalname); } }); const fileFilter = (req, file, cb) => { if ( file.mimetype === "image/png" || file.mimetype === "image/jpg" || file.mimetype === "image/jpeg" ) { cb(null, true); } else { cb(null, false); } }; app.use(bodyParser.json()); app.use( multer({ storage: fileStorage, fileFilter: fileFilter }).single("image") ); .catch(err => console.log(err));
And here's your Controller (feed.js):
exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error("No image provided"); errorStatusCode = 422; throw error; } const imageUrl = req.file.path.replace("\\", "/"); const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: imageUrl, creator: { name: "Bruce" } }); post .save() .then(result => { console.log(result); res.status(201).json({ message: "Post created successfully", post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
"multer" is going to sense whether or not you're in the zone of a "multi-form" kind of posting. If you are, that will kick in automatically and you're now processing your binary files appropriately. One thing that surfaced in the midst of all this is that when you're using multer in a Windows format is that you have to use "filename." I have that highlighted, but that was something different than what was presented in the tutorial.The tutorial also had a data being attached to the end of the file, but it was positioned after the file extension, so I just deleted it and it worked fine! H) Updating Posts (back to top...) To update a post you'll use the code I've got below. Here's your route: feed.js (router)
const express = require("express"); const { body } = require("express-validator/check"); const feedController = require("../controllers/feed"); const router = express.Router(); //GET /feed/posts router.get("/posts", feedController.getPosts); //POST /feed/post router.post( "/post", [ body("title") .trim() .isLength({ min: 5 }), body("content") .trim() .isLength({ min: 5 }) ], feedController.createPost ); router.get("/post/:postId", feedController.getPost); router.put( "/post/:postId", [ body("title") .trim() .isLength({ min: 5 }), body("content") .trim() .isLength({ min: 5 }) ], feedController.updatePost ); module.exports = router;
Here's your Controller: feed.js (Controller)
const fs = require("fs"); const path = require("path"); const { validationResult } = require("express-validator/check"); const Post = require("../models/post"); exports.getPosts = (req, res, next) => { Post.find() .then(posts => { res .status(200) .json({ message: "Fetched posts successfully.", posts: posts }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error("No image provided"); errorStatusCode = 422; throw error; } const imageUrl = req.file.path.replace("\\", "/"); const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: imageUrl, creator: { name: "Bruce" } }); post .save() .then(result => { console.log(result); res.status(201).json({ message: "Post created successfully", post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; exports.getPost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } console.log("good"); res.status(200).json({ message: "Post fetched.", post: post }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; exports.updatePost = (req, res, next) => { const postId = req.params.postId; console.log("hello" + postId); const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; let imageUrl = req.body.image; if (req.file) { imageUrl = req.file.path; } if (!imageUrl) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } Post.findById(postId) .then(post => { if (!post) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } if (imageUrl !== post.imageUrl) { clearImage(post.imageUrl); } post.title = title; post.imageUrl = imageUrl; post.content = content; return post.save(); }) .then(result => { res.status(200).json({ message: "Post created", post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; const clearImage = filePath => { filePath = path.join(__dirname, "..", filePath); fs.unlink(filePath, err => console.log(err)); };
...and here's your app.js file: app.js
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const mongoose = require("mongoose"); const multer = require("multer"); //const uuidv4 = require("uuid/v4"); const feedRoutes = require("./routes/feed"); const app = express(); const fileStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "images"); }, filename: (req, file, cb) => { //cb(null, uuidv4()); cb(null, file.originalname); } }); const fileFilter = (req, file, cb) => { if ( file.mimetype === "image/png" || file.mimetype === "image/jpg" || file.mimetype === "image/jpeg" ) { cb(null, true); } else { cb(null, false); } }; app.use(bodyParser.json()); app.use( multer({ storage: fileStorage, fileFilter: fileFilter }).single("image") ); app.use("/images", express.static(path.join(__dirname, "images"))); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE" ); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); next(); }); app.use("/feed", feedRoutes); app.use((error, req, res, next) => { console.log(error); const status = error.statusCode; const message = error.message; res.status(status).json({ message: message }); }); mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/messages" ) .then(result => { app.listen(8080); }) .catch(err => console.log(err));
I) Deleting Posts (back to top...) First of all, establish your route in your "api" directory. Remember, your "react" content is nothing other than your GUI. The "nuts and bolts" of your app is primarily your "api" code. 1) feed.js (route) (back to top...) In your "api" code, you've go this for your feed.js in your router directory: router.delete("/post/:postId", feedController.deletePost); 2) feed.js (controller) (back to top...) Here's what we've got for our Controller...
exports.deletePost = (req, res, next) => { const postId = req.params.postId; // grab the product ID Post.findById(postId) .then(post => { // we used this code before to find out if we've got a product in the database if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } clearImage(post.imageUrl); // kill the image return Post.findByIdAndRemove(postId); // this is your delete code }) .then(result => { console.log(result); res.status(200).json({ message: "Deleted post!" }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
1
"return," in the context of conventional function would route the code in a way that ignores whatever follows. At that point, you're out the door. Here, however, you're using "return" in the context of a callback / promise. As a result, it's throught more in terms of how the next line of code is going to be receiving whatever proceeds it. In this case, the next "then" clause is receiving the result from "findByIdAndRemove." J) Pagination (back to top...) We're going to limit the number of records we return and then make use of some of the code that's already been written on our front end. Take a look: 1) feed.js (front end) (back to top...) Feed.js (react->src->Pages->feed)
loadPosts = direction => { if (direction) { this.setState({ postsLoading: true, posts: [] }); } let page = this.state.postPage; if (direction === 'next') { page++; this.setState({ postPage: page }); } if (direction === 'previous') { page--; this.setState({ postPage: page }); } fetch('http://localhost:8080/feed/posts?page=' + page) here's the URL that you're getting your info from. Notice the "+ page" dynamic. This is different than what you had before. You'll be passing the page value back into your backend code. .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch posts'); } return res.json(); }) .then(resData => { this.setState({ posts: resData.posts.map(post => { return { ...post, imagePath: post.imageUrl }; }), totalPosts: resData.totalItems, postsLoading: false }); }) .catch(this.catchError); };
The portion of the above code that you have highlighted? That's being passed into your backend... 2) feed.js (Controller) (back to top...)
exports.getPosts = (req, res, next) => { const currentPage = req.query.page || 1; const perPage = 2; let totalItems; Post.find() // "post" refers to your model which is your "post" table .countDocuments() .then(count => { totalItems = count; return Post.find() // be sure to refer to the supplementary info about how" return works in this context .skip((currentPage - 1) * perPage) .limit(perPage); }) .then(posts => { res.status(200).json({ message: "Fetched posts successfully", posts: posts, totalItems: totalItems }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
1
establish a currentPage value to the incoming "page" value. Use the "||" shortcut to signify that if the incoming "page" dynamic doesn't exist, the system defaults to page "1."
2
the "perPage" value refers to the number of rows you're going to have displayed on your page todal.
3
variable that will house the total number of rows that you're going to be retirieving
4
function that counts the number of incoming records. If that function is successful, the code proceeds to the next block where...
5
the "totalItems" variable is assigned the value of "count," or what came out of the "countDocuments" function.
6
I then am going to "return" ther result of "Post.find." Bear in mind that while you may have grown comfortable processing "return" as an obligatory" disruption of the code, the same thing is happening, but... ...you're operating in the context of several "chained" functions that are coupled together. So, now a "return" is going to simply transport the result of that functionality to the next block of code. Here's a little more info about all that from EE:
You still looking at it in the correct context, but the bit I think you're missing is that you're actually chaining anonymous functions together. Your return statement does exit the flow of the function it's contained in. Instead of using anonymous function, think of it more like this:
function DeletePost(post) { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } //check login user clearImage(post.imageUrl); return Post.findByIdAndRemove(postId); }
If you expand out your anonymous functions to full functions, it might be a little clearer:
Post.findById(postId) .then(DeletePost) .then(OutputTheResult) .catch(ErrorHandler); function DeletePost(post) { // the findById returns a Post (by resolving a promise) // that gets passed into this function which we can use in here // and we can also return something from this function which will be passed into the next if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } //check login user clearImage(post.imageUrl); return Post.findByIdAndRemove(postId); } function OutputTheResult(result) { // result is what gets returned from the previous function console.log(result); res.status(200).json({ message: "Deleted post!" }); } function ErrorHandler(err) { // Oops - Something went wrong! }
Your return is still just returning a value from a function. It's just that when you chain your functions together like that, the value returned is just automatically passed as a parameter into the next function. Take a look a this:
var myResult = DoSomething(); var nextResult = AnotherFunction( myResult ); var lastResult = FinalFunction( nextResult ); console.log( lastResult );
All we're doing here is manually assigning the return value of a function to variable. We're then passing that variable into another function, and so on and so on :) The reason we chain like this using promises is because when we call an Async function, we never know when that function will complete (it might take a millisecond / it might take 10 seconds), but it does promise to return at some point. With a promise, you then call the next function, passing in the result of previous call.
7
skip lets you "skip" over a specified number of rows. In this case, I know I've got 2 rows per page. So, if I'm on page 2, that means there's a total of 4 rows that I need to skip in order to ensure that when I advance to the next page, I'll be looking at the fifth row. But... If I'm on page 1, which is my starting point, I don't want to begin with the 3rd row. So to keep everything accurate, I'll subtract "1" from whatever represents my "currentPage" value and that will ensure an accurate starting point for every page.
8
"limit" allows me to limit the number of rows and I'm using my "perPage" value to dictate the number of rows I'm going to display at one time.
9
we're going to respond with not only some data, but also the "totalItems" variable which our front end is expecting in this line: totalPosts: resData.totalItems As far as where your "next" and your "previous" buttons are coming from, you can find that in your "Paginator.js" file. K) Building a User Model (back to top...) Easy! Here we go: auth.js (routes) const express = require("express"); const router = express.Router(); router.put("/signup"); module.exports = router; user.js (model)
const mongoose = require('mongoos'); const Schema = mongoose.Schema; const userSchema = new Schema({ email: { type: String, required: true }, password: { type: String, required: true }, name: { type: String, required: true }, status { type: String, required: true }, posts: [ { type: Schema.types.ObjectId, ref: 'Post' } ] }); module.exports = mongoose.model('User', userSchema);
app.js const authRoutes = require("./routes/auth"); ... app.use("/auth", authRoutes); L) Adding User Validation (back to top...) Here's your auth.js route where you're checking to see if the incoming email already exists and then you're checking for flawed entries:
const express = require("express"); const { body } = require("express-validator/check"); const User = require("../models/user"); const authController = require("../controllers/auth"); const router = express.Router(); router.put( "/signup", [ body("email") .isEmail() .withMessage("Please enter a valid email.") .custom((value, { req }) => { return User.findOne({ email: value }).then(userDoc => { if (userDoc) { return Promise.reject("Email already exists!"); } }); }) .normalizeEmail(), body("password") .trim() .isLength({ min: 5 }), body("name") .trim() .not() .isEmpty() ], authController.signup ); module.exports = router;
1
anything specified in the context of the body object is going to be evaluated. Errors will be captured in the "validationResult" object which you'll look at in the "auth.js" Controller...
const User = require("..model/user"); const { validationResult } = require("express-validator/check"); exports.signup = (req, res, next) => { const errors = validationResult(req); // here's where you're grabbing the errors if (!errors.isEmpty()) { const error = new Error("Validation failed!"); error.statusCode = 422; error.data = errors.array(); // here's what you'll be passing to your app.js file throw error; } const email = req.body.email; const name = req.body.name; const password = req.body.password; };
...and here's the relevant portion of your app.js file: app.use((error, req, res, next) => { console.log(error); const status = error.statusCode || 500; const message = error.message; const data = error.data; res.status(status).json({ message: message, data: data }); }); M) Signing Users Up (back to top...) 1) bcryptjs (back to top...) Start by installing bcryptjs for the sake of encrypting your incoming data. npm install --save bcyrptjs 2) auth.js (Controller) (back to top...) After the validation piece, we now grab the incoming data which has now been vetted and we insert it into the database. Here's the entire auth.js code thus far with the newest addtions highlighted:
const { validationResult } = require("express-validator/check"); const bcrypt = require('bcryptjs'); const User = require("../models/user"); exports.signup = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed!"); error.statusCode = 422; error.data = errors.array(); throw error; } const email = req.body.email; const name = req.body.name; const password = req.body.password; bcrypt .hash(password, 12) .then(hashedPw => { const user = new User({ email: email, password: hashedPw, name: name }); return user.save(); }) .then(result => { res.status(201) .json({message: 'User created!', userId: result._id }); }) .catch(err => { if(!err.statusCode) { err.StatusCode = 500; } next(err); }); };
3) App.js (front end)(back to top...) On your "App.js" file, start by resetting the "isAuth" value to false, which is at the top of the page: class App extends Component { state = { showBackdrop: false, showMobileNav: false, isAuth: false, token: null, userId: null, authLoading: false, error: null }; ...and then you've got these adjustments you've made to your "signupHandler..."
signupHandler = (event, authData) => { event.preventDefault(); this.setState({ authLoading: true }); fetch("http://localhost:8080/auth/signup", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: authData.signupForm.email.value, password: authData.signupForm.password.value, name: authData.signupForm.name.value }) })
N) How Validation Works (JWT) (back to top...) The term "stateless" refers to the way in which APIs don't store session data. Whereas in the context of a more conventional web app you had session variables, with this dynamic you have instead JWT - Jason Web Tokens. YOu can see that illustrated below...
The token itself is going to be a collection of JSON data including the "signature" and the "payload." O) Validating User Login (back to top...) 1) auth.js (Controller) (back to top...) Here's the first piece of the authentication pie...
exports.login = (req, res, next) => { const email = req.body.email; const password = req.body.password; let loadedUser; User.findOne({ email: email }) .then(user => { if(!user) = { const error = new Error('A user with this email could not be found.'); error.statusCode = 401; throw error; } loadedUser = user; return bcrypt.compare(password, user.password); }) .then(isEqual => { if(!isEqual) { const error = new Error('Wrong password!'); error.statusCode = 401; throw error; } //right here }) .catch(err => { if(!err.statusCode) { err.StatusCode = 500; } next(err); }); };
1
establish the "loadedUser" variable
2
find a row in the database that matches the incoming email
3
if an email is found to match, then assign the user object to "loadedUser"
4
return the result of the comparison between the incoming password and the password that's in the database
5
if the returned value "isn't equal," then throw an error. If we're good to go, then we move forward with establishing a JWT... P) Logging in and Creating JSON Web Tokens (JWTs) (back to top...) 1) auth.js (route) (back to top...) Your route is pretty straight forward... router.post('/login', authController.login); 2) auth.js (controller) (back to top...) Again, pretty straight forward...
exports.login = (req, res, next) => { const email = req.body.email; const password = req.body.password; let loadedUser; User.findOne({ email: email }) .then(user => { if(!user) { const error = new Error('A user with this email could not be found.'); error.statusCode = 401; throw error; } loadedUser = user; return bcrypt.compare(password, user.password); }) .then(isEqual => { if(!isEqual) { const error = new Error('Wrong password!'); error.statusCode = 401; throw error; } const token = jwt.sign({ email: loadedUser.email, userId: loadedUser._id.toString() }, 'secret', { expiresIn: '1h' } ); res.status(200).json({ token: token, userId: loadedUser._id.toString() }); }) .catch(err => { if(!err.statusCode) { err.StatusCode = 500; } next(err); }); };
3) App.js (front end) Front end... BAM!
loginHandler = (event, authData) => { event.preventDefault(); this.setState({ authLoading: true }); fetch("http://localhost:8080/auth/login", { method: "Post", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: authData.email, password: authData.password }) }) .then(res => { if (res.status === 422) { throw new Error("Validation failed."); } if (res.status !== 200 && res.status !== 201) { console.log("Error!"); throw new Error("Could not authenticate you!"); } return res.json(); }) .then(resData => { console.log(resData); this.setState({ isAuth: true, token: resData.token, authLoading: false, userId: resData.userId }); localStorage.setItem("token", resData.token); localStorage.setItem("userId", resData.userId); const remainingMilliseconds = 60 * 60 * 1000; const expiryDate = new Date( new Date().getTime() + remainingMilliseconds ); localStorage.setItem("expiryDate", expiryDate.toISOString()); this.setAutoLogout(remainingMilliseconds); }) .catch(err => { console.log(err); this.setState({ isAuth: false, authLoading: false, error: err }); }); };
Q) Using and Validating the Token (back to top...) 1) Feed.js) (back to top...) You're generating a token, now you've got to ensure that it exists and that it's valid. To do that, you're going to first adjust your "Feed.js" file in your "react" application so that your header includes the "Authorization" dynamic. This is in your "src/Pages/feed" directory. This was enabled in your "app.js" file in your "api" in your "app.js" file: app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE" ); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); next(); }); Here's the way your "Feed.js" file is going to look with the new "header..."
loadPosts = direction => { if (direction) { this.setState({ postsLoading: true, posts: [] }); } let page = this.state.postPage; if (direction === 'next') { page++; this.setState({ postPage: page }); } if (direction === 'previous') { page--; this.setState({ postPage: page }); } fetch('http://localhost:8080/feed/posts?page=' + page, { headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch posts'); } return res.json(); }) .then(resData => { this.setState({ posts: resData.posts.map(post => { return { ...post, imagePath: post.imageUrl }; }), totalPosts: resData.totalItems, postsLoading: false }); }) .catch(this.catchError); };
So, there's your header which you recognize from the work that you've done before with Fitbit. 2) is-auth.js (back to top...) Your "is-auth.js" page is Middleware which is kept in the brand new Middleware directory you need to make in your "api" project. Here's your file:
const jwt = require('jsonwebtoken'); module.exports = (req, res, next) => { const authHeader = req.get('Authorization'); if(!authHeader) { const error = new Error('Not Authenticated'); error.statusCode = 401; throw error; } const token = req.get('Authorization').split(' ')[1]; let decodedToken; try { decodedToken = jwt.verify(token, 'secret'); } catch (err) { err.statusCode = 500; throw err; } if(!decodedToken) { const error = new Error('Not Authenticated'); error.statuscode = 401; throw error; } req.userID = decodedToken.userId; next(); };
1
bring in your "jsonwebtoken" package. You're going to need that...
2
make sure there IS an incoming token. Otherwise, throw an error...
3
here comes your token via the header that you set up earlier (Authorization: 'Bearer ' + this.props.token)
4
here's where you're unpacking your token and seeing if it's valid. For that you're going to need the signature that was defined earlier in your "auth.js" file within your "api" directory (see below).
exports.login = (req, res, next) => { const email = req.body.email; const password = req.body.password; let loadedUser; User.findOne({ email: email }) .then(user => { if(!user) { const error = new Error('A user with this email could not be found.'); error.statusCode = 401; throw error; } loadedUser = user; return bcrypt.compare(password, user.password); }) .then(isEqual => { if(!isEqual) { const error = new Error('Wrong password!'); error.statusCode = 401; throw error; } const token = jwt.sign({ email: loadedUser.email, userId: loadedUser._id.toString() }, 'secret', { expiresIn: '1h' } ); res.status(200).json({ token: token, userId: loadedUser._id.toString() }); }) .catch(err => { if(!err.statusCode) { err.StatusCode = 500; } next(err); }); };
5
if something goes south with your attempt to decode the incoming token, throw an error
6
you've successfully decoded the incoming token, but you've not been verified, so you're not going anywhere
7
all is well and you're moving foward as a validated user! 3) feed.js (route) (back to top...) You're going to include this new "is-auth.js" file in the context of your "feed.js" route. This is middleware. We've talked about this before, but for the sake of review, remember that with middleware you're accessing the request and the response objects as well as what represents the "next" middleware function in the application's request-response cycle. Here's your new "feed.js" route with the new additions highlighted:
const express = require("express"); const { body } = require("express-validator/check"); const feedController = require("../controllers/feed"); const isAuth = require('../middleware/is-auth'); const router = express.Router(); //GET /feed/posts router.get("/posts", isAuth, feedController.getPosts); //POST /feed/post router.post( "/post", [ body("title") .trim() .isLength({ min: 5 }), body("content") .trim() .isLength({ min: 5 }) ], feedController.createPost ); router.get("/post/:postId", feedController.getPost); router.put( "/post/:postId", [ body("title") .trim() .isLength({ min: 5 }), body("content") .trim() .isLength({ min: 5 }) ], feedController.updatePost ); router.delete("/post/:postId", feedController.deletePost); module.exports = router;
R) Adding Auth Middleware to All Routes and Methods (back to top...) 1) Feed.js (front end) (back to top...) To add the authorization dynamic that we just installed to all routes / front end, it's going to look like this. Basically, you're adding Authorization to the edit and delete functionality.
import React, { Component, Fragment } from 'react'; import Post from '../../components/Feed/Post/Post'; import Button from '../../components/Button/Button'; import FeedEdit from '../../components/Feed/FeedEdit/FeedEdit'; import Input from '../../components/Form/Input/Input'; import Paginator from '../../components/Paginator/Paginator'; import Loader from '../../components/Loader/Loader'; import ErrorHandler from '../../components/ErrorHandler/ErrorHandler'; import './Feed.css'; class Feed extends Component { state = { isEditing: false, posts: [], totalPosts: 0, editPost: null, status: '', postPage: 1, postsLoading: true, editLoading: false }; componentDidMount() { fetch('URL') .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch user status.'); } return res.json(); }) .then(resData => { this.setState({ status: resData.status }); }) .catch(this.catchError); this.loadPosts(); } loadPosts = direction => { if (direction) { this.setState({ postsLoading: true, posts: [] }); } let page = this.state.postPage; if (direction === 'next') { page++; this.setState({ postPage: page }); } if (direction === 'previous') { page--; this.setState({ postPage: page }); } fetch('http://localhost:8080/feed/posts?page=' + page, { headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch posts'); } return res.json(); }) .then(resData => { this.setState({ posts: resData.posts.map(post => { return { ...post, imagePath: post.imageUrl }; }), totalPosts: resData.totalItems, postsLoading: false }); }) .catch(this.catchError); }; statusUpdateHandler = event => { event.preventDefault(); fetch('URL') .then(res => { if (res.status !== 200 && res.status !== 201) { throw new Error("Can't update status!"); } return res.json(); }) .then(resData => { console.log(resData); }) .catch(this.catchError); }; newPostHandler = () => { this.setState({ isEditing: true }); }; startEditPostHandler = postId => { this.setState(prevState => { const loadedPost = { ...prevState.posts.find(p => p._id === postId) }; return { isEditing: true, editPost: loadedPost }; }); }; cancelEditHandler = () => { this.setState({ isEditing: false, editPost: null }); }; finishEditHandler = postData => { this.setState({ editLoading: true }); const formData = new FormData(); formData.append('title', postData.title); formData.append('content', postData.content); formData.append('image', postData.image); let url = 'http://localhost:8080/feed/post'; let method = 'POST'; if (this.state.editPost) { url = 'http://localhost:8080/feed/post/' + this.state.editPost._id; method = 'PUT'; } fetch(url, { method: method, body: formData, headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200 && res.status !== 201) { throw new Error('Creating or editing a post failed!'); } return res.json(); }) .then(resData => { console.log(resData); const post = { _id: resData.post._id, title: resData.post.title, content: resData.post.content, creator: resData.post.creator, createdAt: resData.post.createdAt }; this.setState(prevState => { let updatedPosts = [...prevState.posts]; if (prevState.editPost) { const postIndex = prevState.posts.findIndex( p => p._id === prevState.editPost._id ); updatedPosts[postIndex] = post; } else if (prevState.posts.length < 2) { updatedPosts = prevState.posts.concat(post); } return { posts: updatedPosts, isEditing: false, editPost: null, editLoading: false }; }); }) .catch(err => { console.log(err); this.setState({ isEditing: false, editPost: null, editLoading: false, error: err }); }); }; statusInputChangeHandler = (input, value) => { this.setState({ status: value }); }; deletePostHandler = postId => { this.setState({ postsLoading: true }); fetch('http://localhost:8080/feed/post/' + postId, { method: 'DELETE', headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200 && res.status !== 201) { throw new Error('Deleting a post failed!'); } return res.json(); }) .then(resData => { console.log(resData); this.setState(prevState => { const updatedPosts = prevState.posts.filter(p => p._id !== postId); return { posts: updatedPosts, postsLoading: false }; }); }) .catch(err => { console.log(err); this.setState({ postsLoading: false }); }); }; errorHandler = () => { this.setState({ error: null }); }; catchError = error => { this.setState({ error: error }); }; render() { return ( <Fragment> <ErrorHandler error={this.state.error} onHandle={this.errorHandler} /> <FeedEdit editing={this.state.isEditing} selectedPost={this.state.editPost} loading={this.state.editLoading} onCancelEdit={this.cancelEditHandler} onFinishEdit={this.finishEditHandler} /> <section className="feed__status"> <form onSubmit={this.statusUpdateHandler}> <Input type="text" placeholder="Your status" control="input" onChange={this.statusInputChangeHandler} value={this.state.status} /> <Button mode="flat" type="submit"> Update </Button> </form> </section> <section className="feed__control"> <Button mode="raised" design="accent" onClick={this.newPostHandler}> New Post </Button> </section> <section className="feed"> {this.state.postsLoading && ( <div style={{ textAlign: 'center', marginTop: '2rem' }}> <Loader /> </div> )} {this.state.posts.length <= 0 && !this.state.postsLoading ? ( <p style={{ textAlign: 'center' }}>No posts found.</p> ) : null} {!this.state.postsLoading && ( <Paginator onPrevious={this.loadPosts.bind(this, 'previous')} onNext={this.loadPosts.bind(this, 'next')} lastPage={Math.ceil(this.state.totalPosts / 2)} currentPage={this.state.postPage} > {this.state.posts.map(post => ( <Post key={post._id} id={post._id} author={post.creator.name} date={new Date(post.createdAt).toLocaleDateString('en-US')} title={post.title} image={post.imageUrl} content={post.content} onStartEdit={this.startEditPostHandler.bind(this, post._id)} onDelete={this.deletePostHandler.bind(this, post._id)} /> ))} </Paginator> )} </section> </Fragment> ); } } export default Feed;
1) SinglePost.js Here's your SinglePost.js "single post" display front end:
import React, { Component } from 'react'; import Image from '../../../components/Image/Image'; import './SinglePost.css'; class SinglePost extends Component { state = { title: '', author: '', date: '', image: '', content: '' }; componentDidMount() { const postId = this.props.match.params.postId; if(!postId) { fetch('http://localhost:8080/feed/post/' + postId, { headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch status'); } return res.json(); }) .then(resData => { this.setState({ title: resData.post.title, author: resData.post.creator.name, date: new Date(resData.post.createdAt).toLocaleDateString('en-US'), content: resData.post.content }); }) .catch(err => { console.log(err); }); } } render() { return ( <section className="single-post"> <h1>{this.state.title}</h1> <h2> Created by {this.state.author} on {this.state.date} </h2> <div className="single-post__image"> <Image contain imageUrl={this.state.image} /> </div> <p>{this.state.content}</p> </section> ); } } export default SinglePost;
S) Connecting Posts to Users (back to top...) 1) post.js (model) (back to top...) To connect users to their corresponding posts, you're going to start with your "post.js" model in your back end (api/models). post.js
const mongoose = require("mongoose"); const Schema = mongoose.Schema; const postSchema = new Schema( { title: { type: String, required: true }, imageUrl: { type: String, required: true }, content: { type: String, required: true }, creator: { type: Schema.Types.ObjectId, ref: 'User', required: true } }, { timestamps: true } ); module.exports = mongoose.model("Post", postSchema);
2) feed.js (controller) (back to top...) Now that you've got your model squared away, let's hit the "feed.js" controller...
exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error("No image provided"); errorStatusCode = 422; throw error; } const imageUrl = req.file.path.replace("\\", "/"); const title = req.body.title; const content = req.body.content; let creator; const post = new Post({ title: title, content: content, imageUrl: imageUrl, //creator: { name: "Bruce" } //req.userId is available now through "is-auth.js" creator: req.userId }); post .save() .then(result => { return User.findById(req.userId); }) .then(user=>{ creator = user; user.posts.push(post); //this is going to work now because of the relationships we set up in the "posts" model return user.save(); }) .then(result => { res.status(201).json({ message: "Post created successfully", post: result, creator: {_id: creator._id, name: creator.name} }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
1
this is the first thing you're going to adjust. Rather than the creator being a hard coded name, you're going to retrieve the user from the "user" table. To do that, you're going to need the userId which is now available thanks to your newly installed "authorization" dynamic via the "is-auth.js" code... req.userID = decodedToken.userId; So, now "creator" is set to "req.userId."
2
Now, as part of the "saving" process, we're going to look up the user info based on the userId. We've got the User model referenced at the top of the page now... const User = require("../models/user"); ...so now, with that little bit of code, "User.findById(req.userId)," we've got access to all that info contanined within that row of data.
3
If you refer back to your User model, you've got this: posts: [ { type: Schema.Types.ObjectId, ref: "Post" } ] By doing user.posts.push(post);, you're pushing the postId into the "user" table. Node takes care of all the heavy lifting.
4
add a "creator" variable that's uninitialized that we're going to use at
5
now you're going to pack all of the elements contained with the "user" object into the "creator" variable
6
now, with a new "creator" variable in place that has all of my "user" info, I'm going to set up "creator" as a key and then with my curly braces, assign some accessible pieces of info including the "_id" and the "name" of the user that just added a post.
BTW: Ran into an error when I first ran this code! It read: "Post validation failed: creator: Path `creator` is required." The problem was in your "createPost" method, you referred to your creator as req.UserId when you defined in your "is-auth.js" code as user.ID. That showed up in User.findById(req.userId) and creator: req.userId. Make a note...
T) Adding Authorization Checks (back to top...) To ensure that only the user who created the resource in question is allowed to either update it or delete it, we add the code that you see highlighted below. It's pretty straight forward. Basically, we're just throwing an error if the userID associated with the post doesn't match the Id of the user that's currently logged in.
exports.updatePost = (req, res, next) => { const postId = req.params.postId; const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; let imageUrl = req.body.image; if (req.file) { imageUrl = req.file.path; } if (!imageUrl) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } Post.findById(postId) .then(post => { if (!post) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } if(post.creator.toString() !== req.userID) { const error = new Error('You are not authorized to fool with this post!'); error.statusCode = 403; throw error; } if (imageUrl !== post.imageUrl) { clearImage(post.imageUrl); } post.title = title; post.imageUrl = imageUrl; post.content = content; return post.save(); }) .then(result => { res.status(200).json({ message: "Post updated", post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; exports.deletePost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } if(post.creator.toString() !== req.userID) { const error = new Error('You are not authorized to fool with this post!'); error.statusCode = 403; throw error; } //check login user clearImage(post.imageUrl); return Post.findByIdAndRemove(postId); }) .then(result => { console.log(result); res.status(200).json({ message: "Deleted post!" }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
U) Clearing Post-User Relations (back to top...)
BTW: This little number: console.log(Authorization); wouldn't seem to be a dealbreaker, BUT, if you don't include the single quotes around console.log('Authorization'); you will get an error and things will crash.
This is not difficult. You simply include the highlighted code that you see below:
exports.deletePost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } if(post.creator.toString() !== req.userID) { //const error = new Error('You are not authorized to fool with this post!'); const error = new Error(req.userID); error.statusCode = 403; throw error; } //check login user clearImage(post.imageUrl); return Post.findByIdAndRemove(postId); }) .then(result => { return User.findById(req.userID); }) .then(user=> { user.posts.pull(postId); return user.save(); console.log(result); res.status(200).json({ message: "Deleted post!" }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
One thing to bear in mind is to ensure that your "isAuth" is in place within your route. I neglected to ensure this was in place initially and it caused an error on my "is-auth.js" page. It didn't make sense until I realized that my "delete" code was looking for the req.userID value that the "is-auth.js" code provides. Once I put router.delete("/post/:postId", isAuth, feedController.deletePost); in place, everything was gold!
V) Getting Rid of "Unexpected token < in JSON at position 0" (back to top...) You're going to start by adding a new route just so you can see what's going on. "auth.js" is going to look like this:
const express = require("express"); const { body } = require("express-validator/check"); const User = require("../models/user"); const authController = require("../controllers/auth"); const isAuth = require('../middleware/is-auth'); const router = express.Router(); router.put( "/signup", [ body("email") .isEmail() .withMessage("Please enter a valid email.") .custom((value, { req }) => { return User.findOne({ email: value }).then(userDoc => { if (userDoc) { return Promise.reject("Email already exists!"); } }); }) .normalizeEmail(), body("password") .trim() .isLength({ min: 5 }), body("name") .trim() .not() .isEmpty() ], authController.signup ); router.get('/status', isAuth, authController.getUserStatus); router.patch( '/status', isAuth, [ body('status') .trim() .not() .isEmpty() ], authController.updateUserStatus ); module.exports = router;
Don't forget that the actual route for this page is going ot be "auth/status" because of what you've got set up in your "app.js" file within your "api" project (of course). const feedRoutes = require("./routes/feed"); const profileRoutes = require("./routes/profile"); const authRoutes = require("./routes/auth"); Here's what we've got for our "auth.js" controller. We added two new functions...
exports.getUserStatus = (req, res, next) => { console.log(req.userID); User.findById(req.userID) .then(user => { if(!user) { const error = new Error('User not found'); error.statusCode = 404; throw error; } res.status(200).json({status: user.status}); }) .catch(err=> { if(!err.statusCode) { err.StatusCode = 500; } next(err); }); }; exports.updateUserStatus = (req, res, next) => { const newStatus = req.body.status; User.findById(req.userID) .then(user => { if(!user) { const error = new Error('User not found'); error.statusCode = 404; throw error; } user.status = newStatus; return user.save(); }) .then(result => { res.status(200).json({message: 'User updated.'}); }) .catch(err => { if(!err.statusCode) { err.StatusCode = 500; } next(err); }); }
...and then on our front end, we changed the URL in our "Feed.js" file:
componentDidMount() { fetch('http://localhost:8080/auth/status', { added localhost:8080..to replace URL headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('This is my problem now.'); } return res.json(); }) .then(resData => { this.setState({ status: resData.status }); //looking for some resData so, we'll add that to our auth.js controller }) .catch(this.catchError); this.loadPosts(); } ...and then this function... loadPosts = direction => { if (direction) { this.setState({ postsLoading: true, posts: [] }); } let page = this.state.postPage; if (direction === 'next') { page++; this.setState({ postPage: page }); } if (direction === 'previous') { page--; this.setState({ postPage: page }); } fetch('http://localhost:8080/feed/posts?page=' + page, { //Authorization header here and line #131, 187 headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch posts'); } return res.json(); }) .then(resData => { this.setState({ posts: resData.posts.map(post => { return { ...post, imagePath: post.imageUrl }; }), totalPosts: resData.totalItems, postsLoading: false }); }) .catch(this.catchError); };
With Async Await, you're still using ".then" blocks, but they're now operating behind the scenes using the code you see below.
feed.js Controller (before)
  1. exports.getPosts = (req, res, next) => {
  2. const currentPage = req.query.page || 1;
  3. const perPage = 2;
  4. let totalItems;
  5. Post.find()
  6. .countDocuments()
  7. .then(count => {
  8. totalItems = count;
  9. return Post.find()
  10. .skip((currentPage - 1) * perPage)
  11. .limit(perPage);
  12. })
  13. .then(posts => {
  14. res.status(200).json({
  15. message: "Fetched posts successfully",
  16. posts: posts,
  17. totalItems: totalItems
  18. });
  19. })
  20. .catch(err => {
  21. if (!err.statusCode) {
  22. err.statusCode = 500;
  23. }
  24. next(err);
  25. });
  26. };
feed.js Controller (after)
  1. exports.getPosts = async (req, res, next) => {
  2. const currentPage = req.query.page || 1;
  3. const perPage = 2;
  4. let totalItems;
  5. try {
  6. const totalItems = await Post.find().countDocuments();
  7. const posts = await Post.find()
  8. .skip((currentPage - 1) * perPage)
  9. .limit(perPage);
  10. res.status(200).json({
  11. message: "Fetched posts successfully",
  12. posts: posts,
  13. totalItems: totalItems
  14. });
  15. } catch (err) {
  16. if(!err.statusCode) {
  17. err.statusCode = 500;
  18. }
  19. next(err);
  20. }
  21. };
Here are some more examples of "before" and "after" where transforming the existing code to an "await" dynamic... Before (just look at all of your major functionality)...
const fs = require("fs"); const path = require("path"); const { validationResult } = require("express-validator/check"); const Post = require("../models/post"); const User = require("../models/user"); exports.getPosts = async (req, res, next) => { const currentPage = req.query.page || 1; let totalItems; try { const totalItems = await Post.find().countDocuments(); const posts = await Post.find() .skip((currentPage - 1) * perPage) .limit(perPage); res.status(200).json({ message: "Fetched posts successfully", posts: posts, totalItems: totalItems }); } catch (err) { if(!err.statusCode) { err.statusCode = 500; } next(err); } }; exports.createPost = async (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('Validation failed, entered data is incorrect.'); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error('No image provided.'); error.statusCode = 422; throw error; } const imageUrl = req.file.path; const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: imageUrl, creator: req.userID }); try { await post.save(); const user = await User.findById(req.userID); user.posts.push(post); await user.save(); res.status(201).json({ message: 'Post created successfully!', post: post, creator: { _id: user._id, name: user.name } }); }; //9:56 exports.getPost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } console.log("good"); res.status(200).json({ message: "Post fetched.", post: post }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; exports.updatePost = (req, res, next) => { const postId = req.params.postId; const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; let imageUrl = req.body.image; if (req.file) { imageUrl = req.file.path; } if (!imageUrl) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } Post.findById(postId) .then(post => { if (!post) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } if(post.creator.toString() !== req.userID) { const error = new Error('You are not authorized to fool with this post!'); error.statusCode = 403; throw error; } if (imageUrl !== post.imageUrl) { clearImage(post.imageUrl); } post.title = title; post.imageUrl = imageUrl; post.content = content; return post.save(); }) .then(result => { res.status(200).json({ message: "Post updated", post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; exports.deletePost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } if(post.creator.toString() !== req.userID) { //const error = new Error('You are not authorized to fool with this post!'); const error = new Error(req.userID); error.statusCode = 403; throw error; } //check login user clearImage(post.imageUrl); return Post.findByIdAndRemove(postId); }) .then(result => { return User.findById(req.userID); }) .then(user=> { user.posts.pull(postId); return user.save(); console.log(result); res.status(200).json({ message: "Deleted post!" }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; const clearImage = filePath => { filePath = path.join(__dirname, "..", filePath); fs.unlink(filePath, err => console.log(err)); };
After...
const fs = require("fs"); const path = require("path"); const { validationResult } = require("express-validator/check"); const Post = require("../models/post"); const User = require("../models/user"); exports.getPosts = async (req, res, next) => { const currentPage = req.query.page || 1; const perPage = 2; let totalItems; try { const totalItems = await Post.find().countDocuments(); const posts = await Post.find() .skip((currentPage - 1) * perPage) .limit(perPage); res.status(200).json({ message: "Fetched posts successfully", posts: posts, totalItems: totalItems }); } catch (err) { if(!err.statusCode) { err.statusCode = 500; } next(err); } }; exports.createPost = async (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('Validation failed, entered data is incorrect.'); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error('No image provided.'); error.statusCode = 422; throw error; } const imageUrl = req.file.path; const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: imageUrl, creator: req.userID }); try { await post.save(); const user = await User.findById(req.userID); user.posts.push(post); await user.save(); res.status(201).json({ message: 'Post created successfully!', post: post, creator: { _id: user._id, name: user.name } }); } catch (err) { if(!err.statusCode) { err.statusCode = 500; } next(err); } }; //9:56 exports.getPost = async (req, res, next) => { console.log("hit it"); const postId = req.params.postId; const post = await Post.findById(postId) try { if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } res.status(200).json({ message: "Post fetched.", post: post }); } catch(err) { if (!err.statusCode) { err.statusCode = 500; } next(err); } }; exports.updatePost = async (req, res, next) => { const postId = req.params.postId; const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; let imageUrl = req.body.image; if (req.file) { imageUrl = req.file.path; } if (!imageUrl) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } try { const post = await Post.findById(postId) if (!post) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } if(post.creator.toString() !== req.userID) { const error = new Error('You are not authorized to fool with this post!'); error.statusCode = 403; throw error; } if (imageUrl !== post.imageUrl) { clearImage(post.imageUrl); } post.title = title; post.imageUrl = imageUrl; post.content = content; const result = await post.save(); res.status(200).json({ message: "Post updated", post: result }); } catch(err) { if (!err.statusCode) { err.statusCode = 500; } next(err); } }; exports.deletePost = async (req, res, next) => { const postId = req.params.postId; try { const post = await Post.findById(postId) if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } if(post.creator.toString() !== req.userID) { const error = new Error(req.userID); error.statusCode = 403; throw error; } clearImage(post.imageUrl); await Post.findByIdAndRemove(postId); const user = await User.findById(req.userID); user.posts.pull(postId); await user.save(); res.status(200).json({ message: "Deleted post!" }); } catch(err) { if (!err.statusCode) { err.statusCode = 500; } next(err); } }; const clearImage = filePath => { filePath = path.join(__dirname, "..", filePath); fs.unlink(filePath, err => console.log(err)); };
A) Intro (back to top...)
According to your normal paradgim, your http client is sending a request to the server and the server responds. With this dynamic, the server is sending data to the client without it being initiated by the client. That may not make a whole lot of sense, but it's a very cool tool to have in your backpocket like what you're getting ready to see with this example. You know how you can have two users on the same app and one adds a product and the other user won't know something's been added unless they refresh their page? With websockets the screen refreshes automatically! Very nice! B) socket.io (back to top...) There are a lot of options to choose from where websockets are concerned, but we're going to use something called, "socket.io. C) Making it Work (back to top...) 1) Installation (back to top...) i) app.js (api) (back to top...) Use this to install the "socket.io" package...
$ npm install --save socket.io
...and then you're going to incorporate it into your "app.js" code like this:
mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/messages" ) .then(result => { const server = app.listen(8080); const io = require('socket.io')(server); io.on('connection', socket => { console.log('client connected'); }); }) .catch(err => console.log(err) );
ii) Feed.js (react) (back to top...) Now, you're going to import the front end counterpart to the backend "socket.io" with this:
$ npm install save socket.io-client
You'll then put this near the top of your page... import openSocket from 'socket.io-client'; ...and then add this to the tail end of the "class Feed extends Component" code:
class Feed extends Component { state = { isEditing: false, posts: [], totalPosts: 0, editPost: null, status: '', postPage: 1, postsLoading: true, editLoading: false }; componentDidMount() { fetch('http://localhost:8080/auth/status', { headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('This is my problem now.'); } return res.json(); }) .then(resData => { this.setState({ status: resData.status }); //looking for some resData so, we'll add that to our auth.js controller }) .catch(this.catchError); this.loadPosts(); openSocket('http://localhost:8080'); }
When you run this, you'll get "Client connected" on your terminal. D) Identifying Realtime Potential (back to top...) We're going to move into a more "practical" application of our code and we're going to kick it off by grabbing this code that was provided by the tutorial and simply plug it into our "Feed.js" file. It's "addPost..."
addPost = post => { this.setState(prevState => { const updatedPosts = [...prevState.posts]; if (prevState.postPage === 1) { if (prevState.posts.length >= 2) { updatedPosts.pop(); } updatedPosts.unshift(post); } return { posts: updatedPosts, totalPosts: prevState.totalPosts + 1 }; }); };
That will go right after openSocket('http://localhost:8080');. E) Sharing the IO Instance Across Files (back to top...) To share this websocket dynamic across multiple files, we're going to set up a "socket.js" file and position that in our home directory. That file is going to look like this:
let it; module.exports = { init: httpServer => { io = require('socket.io')(server)(httpServer); return io; }, getIO: () => { if(!io) { throw new Error('Socket.io not initialized!'); } return io; } }
No big deal. Now we're going to reference it as part of our "app.js" file like this: mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/messages" ) .then(result => { const server = app.listen(8080); const io = require('./socket.io').init(server); io.on('connection', socket => { console.log('client connected'); }); }) .catch(err => console.log(err) ); F) Synchronizing POST Additions (back to top...) 1) Adding IO to feed.js (Controller) (back to top...) So, now when we go to our "feed.js" Controller, we'll import it at the top of the page: const io = require('../socket'); ...and that's referencing the "socket.js" file we created a moment ago. And that file is a continuation of what was initialized on the "app.js" file: mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/messages" ) .then(result => { const server = app.listen(8080); const io = require('./socket').init(server); io.on('connection', socket => { console.log('Client connected'); }); }) .catch(err => console.log(err)); Now, I'm going to use that as part of my "createPost" method. What will happen now is that all connected clients will be alerted to the fact that a new post was just created. Here's how you're going to code that:
exports.createPost = async (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('Validation failed, entered data is incorrect.'); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error('No image provided.'); error.statusCode = 422; throw error; } const imageUrl = req.file.path; const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: imageUrl, creator: req.userID }); try { await post.save(); const user = await User.findById(req.userID); user.posts.push(post); await user.save(); io.getIO().emit('posts', {action: 'create', post: post}); res.status(201).json({ message: 'Post created successfully!', post: post, creator: { _id: user._id, name: user.name } }); } catch (err) { if(!err.statusCode) { err.statusCode = 500; } next(err); } };
2) Feed.js (front end) (back to top...) Here's the code you need for the "component didMount" piece:
componentDidMount() { fetch('http://localhost:8080/auth/status', { headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('This is my problem now.'); } return res.json(); }) .then(resData => { this.setState({ status: resData.status }); //looking for some resData so, we'll add that to our auth.js controller }) .catch(this.catchError); this.loadPosts(); const socket = openSocket('http://localhost:8080'); socket.on('posts', data => { if(data.action === 'create') { this.addPost(data.post); } }); }
G) Adding Name to Posts (back to top...) Here's a little tweak to our "feed.js" page that corrects the fact that that name wasn't being properly displayed.
exports.getPosts = async (req, res, next) => { const currentPage = req.query.page || 1; const perPage = 2; let totalItems; try { const totalItems = await Post.find().countDocuments(); const posts = await Post.find() .populate('creator') .skip((currentPage - 1) * perPage) .limit(perPage); res.status(200).json({ message: "Fetched posts successfully", posts: posts, totalItems: totalItems }); } catch (err) { if(!err.statusCode) { err.statusCode = 500; } next(err); } }; exports.createPost = async (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('Validation failed, entered data is incorrect.'); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error('No image provided.'); error.statusCode = 422; throw error; } const imageUrl = req.file.path; const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: imageUrl, creator: req.userID }); try { await post.save(); const user = await User.findById(req.userID); user.posts.push(post); await user.save(); io.getIO().emit('posts', {action: 'create', { ...post._doc, creator: {_id: req.userId, name: user.name} }}); res.status(201).json({ message: 'Post created successfully!', post: post, creator: { _id: user._id, name: user.name } }); } catch (err) { if(!err.statusCode) { err.statusCode = 500; } next(err); } };
1
populate is a Mongo DB function that looks up whaqt amounts to a related field in another table. In this case, "posts" is related to "users" via the field "creator" which holds the ObjectID of the "user."
2
The _doc field lets you access the “raw” document directly, which was delivered through the mongodb driver, bypassing mongoose. There's not a whole lot of info about it. H) Updating Posts on All Connected Clients (back to top...) 1) Controller (back to top...) Here's your Controller:
exports.updatePost = async (req, res, next) => { const postId = req.params.postId; const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error("Validation failed, entered data is incorrect."); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; let imageUrl = req.body.image; if (req.file) { imageUrl = req.file.path; } if (!imageUrl) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } try { const post = await Post.findById(postId).populate('creator'); if (!post) { const error = new Error("No file picked."); error.statusCode = 422; throw error; } if(post.creator._id.toString() !== req.userID) { const error = new Error('You are not authorized to fool with this post!'); error.statusCode = 403; throw error; } if (imageUrl !== post.imageUrl) { clearImage(post.imageUrl); } post.title = title; post.imageUrl = imageUrl; post.content = content; const result = await post.save(); io.getIO().emit('posts', { action: 'update', post: result }); res.status(200).json({ message: "Post updated", post: result }); } catch(err) { if (!err.statusCode) { err.statusCode = 500; } next(err); } };
1
we're using the MongoDB "populate" method to look up and make available the info coming from the users table based on the object ID located in the "creator" column.
2
emit the results of what's just been updated. 2) Feed.js (front end) (back to top...) Here's your "Feed.js" file. This first piece is a new piece of functionality that we added after the "addPost" method.
updatePost = post => { this.setState(prevState => { const updatedPosts = [...prevState.posts]; const updatedPostIndex = updatedPosts.findIndex(p => p._id === post._id); if (updatedPostIndex > -1) { updatedPosts[updatedPostIndex] = post; } return { posts: updatedPosts }; }); };
We also changed the componentDidMount function like this:
componentDidMount() { fetch('http://localhost:8080/auth/status', { headers: { Authorization: 'Bearer ' + this.props.token } }) .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch user status.'); } return res.json(); }) .then(resData => { this.setState({ status: resData.status }); }) .catch(this.catchError); this.loadPosts(); const socket = openSocket('http://localhost:8080'); socket.on('posts', data => { if (data.action === 'create') { this.addPost(data.post); } else if (data.action === 'update') { this.updatePost(data.post); } }); }
And that will do it! I) Sorting Correctly (back to top...) To sort the list of posts according to their timestamp where the most recently added posts show up first, you do this:
exports.getPosts = async (req, res, next) => { const currentPage = req.query.page || 1; const perPage = 2; let totalItems; try { const totalItems = await Post.find().countDocuments(); const posts = await Post.find() .populate('creator') .sort({createdAt: -1}) .skip((currentPage - 1) * perPage) .limit(perPage); res.status(200).json({ message: "Fetched posts successfully", posts: posts, totalItems: totalItems }); } catch (err) { if(!err.statusCode) { err.statusCode = 500; } next(err); } };
J) Deleting Posts (back to top...) 1) feed.js Controller (back to top...) Here's your backend (feed.js / Controller)
exports.deletePost = async (req, res, next) => { const postId = req.params.postId; try { const post = await Post.findById(postId) if (!post) { const error = new Error("Could not find post."); error.statusCode = 404; throw error; } if(post.creator.toString() !== req.userID) { const error = new Error(req.userID); error.statusCode = 403; throw error; } clearImage(post.imageUrl); await Post.findByIdAndRemove(postId); const user = await User.findById(req.userID); user.posts.pull(postId); await user.save(); io.getIO().emit('posts', {action: 'delete', post: postId}); res.status(200).json({ message: "Deleted post!" }); } catch(err) { if (!err.statusCode) { err.statusCode = 500; } next(err); } };
2) Feed.js (front end) (back to top...) A) What is GraphQL (back to top...) GraphQL is a stateless (no session variables), client-independent API for exchanging data with higher query flexibility. Check out the graphic below...
With a conventional REST API dynamic, you've got some limitations in that you're going to retrieve a row based on an ID and you're going to get all of the the corresponding information (findById). What the above graphic demonstrates is that if you want specific information, you need to change the endpoint and that can become cumbersome, especially if it's a large app and your query shows up in multiple places. With GraphQL, you've got more flexibility. How? With GraphQL, you only have one endpoint and it's within that endpoint that you'll specify what it is you're trying to do...
Here's an example of what a typical GraphQL query is going to look like:
Notice you've got your "Operation Type," your endpoint and then you've got your "Requested Fields." It's in the context of your "Requested Fields" that the utility of GraphQL can really be appreciated. As far as "Operation Types," here's a breakdown:
To sum it up, here's a graphic that shows the "flow" of GraphQL and how your Operation Types can be thought of as Routes and your Resolvers (which we'll get into in a minute) can be throught of as Controllers...
B) Setup and Our First Query 1) Setup (back to top...) You're going to delete your "routes" folder and you're also getting rid of all the references to your routes in you "app.js" file within your "api" app. Yeah, we're cleaning house. In addition, you're going to install the "graphql" and "express graphql" packages using this command: $ npm install --save graphql express-graphql 2) Our First Query (back to top...) i) app.js (back to top...) On "app.js," import and implement these files like so (pay attention to the highlighted items:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const mongoose = require("mongoose"); const multer = require("multer"); const graphqlHttp = require('express-graphql'); const graphqlSchema = require('./graphql/schema'); const graphqlResolver = require('./graphql/resolvers'); const app = express(); const fileStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "images"); }, filename: (req, file, cb) => { //cb(null, uuidv4()); cb(null, file.originalname); } }); const fileFilter = (req, file, cb) => { if ( file.mimetype === "image/png" || file.mimetype === "image/jpg" || file.mimetype === "image/jpeg" ) { cb(null, true); } else { cb(null, false); } }; app.use(bodyParser.json()); app.use( multer({ storage: fileStorage, fileFilter: fileFilter }).single("image") ); app.use("/images", express.static(path.join(__dirname, "images"))); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE" ); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); next(); }); app.use('/graphql', graphqlHttp({ schema: graphqlSchema, rootValue: graphqlResolver }) ); app.use((error, req, res, next) => { console.log(error); const status = error.statusCode || 500; const message = error.message; const data = error.data; res.status(status).json({ message: message, data: data }); }); mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/messages" ) .then(result => { const server = app.listen(8080); }) .catch(err => console.log(err));
ii) schema.js and resolvers.js (back to top...) GraphQL is designed to work with a combo of functionalities referenced two files: "schema.js" and "resolvers.js." Here's your "schema.js" filed: const { buildSchema } = require('graphql'); module.exports = buildSchema(` type TestData { text: String! views: Int! } type RootQuery { hello: TestData! } schema { query: RootQuery } `); The logic of this is broken down like this: First of all, "schema" is defined as "...a representation of a plan or theory in the form of an outline or model." It's your basic scaffolding. Going back to the graphic, you can see the three elements depicted. You've got your
1
query which is then defined by the
2
type which is then further detailed as far as the
3
in terms of the actual columns that are being retrieved. That's your schema, but now you need the "resovlers.js" file to actually populate the fields you've targeted with some values. Here's your "resolvers.js" file: module.exports = { hello() { return { text: 'Hello World!', views: 1245 }; } }; You can see where hello is coming from when you compare the "schema.js" file and the "resolvers.js" file... To see all of this in action, you're going to use Postman. The URL is going to be localhost:8080/graphql and you're going to put this in the body... { "query": "{ hello { text views } }" } That gives you this: { "data": { "hello": { "text": "Hello World!", "views": 1245 } } } C) Defining a Mutation (back to top...) 1) schema.js (back to top...)
const { buildSchema } = require('graphql'); module.exports = buildSchema(` type Post { _id: ID! title: String! content: String! imageUrl: String! creator: User! createdAt: String! updatedAt: String! } type User { _id: ID! name: String! email: String! password: String posts: [Post!]! } input UserData { email: String! name: String! password: String! } type RootMutation { createUser(userInput: UserInputData): User! } schema { mutation: RootMutation } `);
module.exports = { createUser(args, req) { //const email = args.userInput.email; } D) Adding a Mutation Resolver & GraphiQL (back to top...) The idea here is to write a query that resonates as a little more practical. We're going to add a user to the users table and it's going to look like this: 1) schema.js (back to top...)
const { buildSchema } = require('graphql'); module.exports = buildSchema(` type Post { _id: ID! title: String! content: String! imageUrl: String! creator: User! createdAt: String! updatedAt: String! } type User { _id: ID! name: String! email: String! password: String posts: [Post!]! } input UserInputData { email: String! name: String! password: String! } type RootQuery { hello: String } type RootMutation { createUser(userInput: UserInputData): User! } schema { query: RootQuery mutation: RootMutation } `);
2) resolvers.js (back to top...)
const bcrypt = require('bcryptjs'); const User = require('../models/user'); module.exports = { createUser: async function({ userInput }, req) { //const email = args.userInput.email; const existingUser = await User.findOne({email: userInput.email}); if(existingUser) { const error = new Error('User exists already!'); throw error; } const hashedPw = await bcrypt.hash(userInput.password, 12); const user = new User({ email: userInput.email, name: userInput.name, password: hashedPw }); const createdUser = await user.save(); return { ...createdUser._doc, _id: createdUser._id.toString() }; } };
3) GraphiQL (back to top...) This is a much better way to proof your code and make some adjustments to it than Postman. You need to add this to your "app.js" file: app.use('/graphql', graphqlHttp({ schema: graphqlSchema, rootValue: graphqlResolver, graphiql:true }) ); Here's a screenshot of the interface:
To get there, you'll got to http://localhost:8080/graphql. E) Adding Validation (back to top...) 1) validator | resolvers.js (back to top...) First thing you're going to do is install validator using: npm install --save validator Here's your "resolver.js" file:
const bcrypt = require('bcryptjs'); const validator = require('validator'); const User = require('../models/user'); module.exports = { createUser: async function({ userInput }, req) { //const email = args.userInput.email; const errors = []; if(!validator.isEmail(userInput.email)) { errors.push({ message: 'Email is invalid.' }); } if( validator.isEmpty(userInput.password) || !validator.isLength(userInput.password, { min: 5 }) ) { errors.push({ message: 'Password too short!' }); } if(errors.length>0) { const error = new Error('Invalid input.'); throw error; } const existingUser = await User.findOne({email: userInput.email}); if(existingUser) { const error = new Error('User exists already!'); throw error; } const hashedPw = await bcrypt.hash(userInput.password, 12); const user = new User({ email: userInput.email, name: userInput.name, password: hashedPw }); const createdUser = await user.save(); return { ...createdUser._doc, _id: createdUser._id.toString() }; } };
1
import the "validator" package. This is the same thing we used with Express.
2
set up an "errors" variable
3
here's some of your validator rules
4
if your errors lengh is greater than 0, throw your error F) Handling Errors (back to top...)
const bcrypt = require('bcryptjs'); const validator = require('validator'); const User = require('../models/user'); module.exports = { createUser: async function({ userInput }, req) { //const email = args.userInput.email; const errors = []; if(!validator.isEmail(userInput.email)) { errors.push({ message: 'Email is invalid.' }); } if( validator.isEmpty(userInput.password) || !validator.isLength(userInput.password, { min: 5 }) ) { errors.push({ message: 'Password too short!' }); } if(errors.length>0) { const error = new Error('Invalid input.'); error.data = errors; error.code = 422; throw error; } const existingUser = await User.findOne({email: userInput.email}); if(existingUser) { const error = new Error('User exists already!'); throw error; } const hashedPw = await bcrypt.hash(userInput.password, 12); const user = new User({ email: userInput.email, name: userInput.name, password: hashedPw }); const createdUser = await user.save(); return { ...createdUser._doc, _id: createdUser._id.toString() }; } };
1
we're adding a "data" field to the array of errors that we created earlier with things like "errors.push({ message: 'Password too short!' });" Now all of those error messages are stored in our "errors" variable. That will be picked up by our "app;js" file like so...
app.use( '/graphql', graphqlHttp({ schema: graphqlSchema, rootValue: graphqlResolver, graphiql:true, formatError(err) { if(!err.originalError) { return err; } const data = err.originalError.data; const message = err.message || 'An error occurred.'; const code = err.originalError.code || 500; return { message: message, status: code, data: data }; } }) );
1
"formatError is a configuration option that is a method that receives the error.
2
an "originalError" is a problem with your code. It's not a technical error like a resource not found or something like that
3
the "originalData" variable is a part of the existing architecture. In our "resolvers.js" file, we tagged on to what the system is going to process the additional errors that we created and we stored them in a variable called "data." Here, we're grabbing that and putting it in a variable that, again, is called, "data."
4
I'm grabbing the message that's coming out of GraphQL and in case it's "undefined," I'll create a generic error. I ran into that, by the way, because my code with the "resolvers.js" file initially looked like this: if(error.length>0) { const error = new Error('Invalid input.'); error.data = errors; error.code = 422; throw error; } "error" should've been "errors." I got an "undefined" as a result of that.
5
grabbing the "code" coming out of the "originalError" and if it's not defined, just call it "500."
6
now return all of the wonderful content that I've either pulled out of "originalError" or created myself on the "resolvers.js" page. G) Hooking Up the Front End (back to top...) 1) CORS Error (back to top...) When I imported the tutorial's front end code, I got this error when I tried to sign up a new user: signup:1 Access to fetch at 'http://localhost:8080/graphql' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status. First of all, CORS stands for "Cross-Origin Resource Sharing." It's a mechanism that allows a website running at one origin to communicate with a server or selected resources residing in a different origin. This is the definition coming from https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS:
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own. An example of a cross-origin request: the front-end JavaScript code served from https://domain-a.com uses XMLHttpRequest to make a request for https://domain-b.com/data.json. For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. For example, XMLHttpRequest and the Fetch API follow the same-origin policy. This means that a web application using those APIs can only request resources from the same origin the application was loaded from, unless the response from other origins includes the right CORS headers.
Here's a graphic to help visualize all this:
So, going back to the error, what's happening is the front end is issuing a request that the API doesn't especially like. This is the piece of code that made all the difference:
app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( 'Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE' ); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); if(req.method==='OPTIONS') { return res.sendStatus(200); } next(); });
Reason being is that Express GraphQL automatically declines anything that isn't a GET or a POST request. So, just for the sake of reference, here's my "App.js" code from the front end:
import React, { Component, Fragment } from 'react'; import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; import Layout from './components/Layout/Layout'; import Backdrop from './components/Backdrop/Backdrop'; import Toolbar from './components/Toolbar/Toolbar'; import MainNavigation from './components/Navigation/MainNavigation/MainNavigation'; import MobileNavigation from './components/Navigation/MobileNavigation/MobileNavigation'; import ErrorHandler from './components/ErrorHandler/ErrorHandler'; import FeedPage from './pages/Feed/Feed'; import SinglePostPage from './pages/Feed/SinglePost/SinglePost'; import LoginPage from './pages/Auth/Login'; import SignupPage from './pages/Auth/Signup'; import './App.css'; class App extends Component { state = { showBackdrop: false, showMobileNav: false, isAuth: false, token: null, userId: null, authLoading: false, error: null }; componentDidMount() { const token = localStorage.getItem('token'); const expiryDate = localStorage.getItem('expiryDate'); if (!token || !expiryDate) { return; } if (new Date(expiryDate) <= new Date()) { this.logoutHandler(); return; } const userId = localStorage.getItem('userId'); const remainingMilliseconds = new Date(expiryDate).getTime() - new Date().getTime(); this.setState({ isAuth: true, token: token, userId: userId }); this.setAutoLogout(remainingMilliseconds); } mobileNavHandler = isOpen => { this.setState({ showMobileNav: isOpen, showBackdrop: isOpen }); }; backdropClickHandler = () => { this.setState({ showBackdrop: false, showMobileNav: false, error: null }); }; logoutHandler = () => { this.setState({ isAuth: false, token: null }); localStorage.removeItem('token'); localStorage.removeItem('expiryDate'); localStorage.removeItem('userId'); }; loginHandler = (event, authData) => { event.preventDefault(); this.setState({ authLoading: true }); fetch('http://localhost:8080/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: authData.email, password: authData.password }) }) .then(res => { if (res.status === 422) { throw new Error('Validation failed.'); } if (res.status !== 200 && res.status !== 201) { console.log('Error!'); throw new Error('Could not authenticate you!'); } return res.json(); }) .then(resData => { console.log(resData); this.setState({ isAuth: true, token: resData.token, authLoading: false, userId: resData.userId }); localStorage.setItem('token', resData.token); localStorage.setItem('userId', resData.userId); const remainingMilliseconds = 60 * 60 * 1000; const expiryDate = new Date( new Date().getTime() + remainingMilliseconds ); localStorage.setItem('expiryDate', expiryDate.toISOString()); this.setAutoLogout(remainingMilliseconds); }) .catch(err => { console.log(err); this.setState({ isAuth: false, authLoading: false, error: err }); }); }; signupHandler = (event, authData) => { event.preventDefault(); this.setState({ authLoading: true }); const graphqlQuery = { query: ` mutation { createUser(userInput: {email: "${ authData.signupForm.email.value }", name:"${authData.signupForm.name.value}", password:"${ authData.signupForm.password.value }"}) { _id email } } ` }; fetch('http://localhost:8080/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(graphqlQuery) }) .then(res => { return res.json(); }) .then(resData => { if (resData.errors && resData.errors[0].status === 422) { throw new Error( "Validation failed. Make sure the email address isn't used yet!" ); } if (resData.errors) { throw new Error('User creation didn\'t happen!'); } console.log(resData); this.setState({ isAuth: false, authLoading: false }); this.props.history.replace('/'); }) .catch(err => { console.log(err); this.setState({ isAuth: false, authLoading: false, error: err }); }); }; setAutoLogout = milliseconds => { setTimeout(() => { this.logoutHandler(); }, milliseconds); }; errorHandler = () => { this.setState({ error: null }); }; render() { let routes = ( ( )} /> ( )} /> ); if (this.state.isAuth) { routes = ( ( )} /> ( )} /> ); } return ( {this.state.showBackdrop && ( )} } mobileNav={ } /> {routes} ); } } export default withRouter(App);
...and here's my "app.js" from my API:
const path = require("path"); const express = require("express"); const bodyParser = require("body-parser"); const mongoose = require("mongoose"); const multer = require("multer"); const graphqlHttp = require('express-graphql'); const graphqlSchema = require('./graphql/schema'); const graphqlResolver = require('./graphql/resolvers'); const app = express(); const fileStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "images"); }, filename: (req, file, cb) => { //cb(null, uuidv4()); cb(null, file.originalname); } }); const fileFilter = (req, file, cb) => { if ( file.mimetype === "image/png" || file.mimetype === "image/jpg" || file.mimetype === "image/jpeg" ) { cb(null, true); } else { cb(null, false); } }; app.use(bodyParser.json()); app.use( multer({ storage: fileStorage, fileFilter: fileFilter }).single("image") ); app.use("/images", express.static(path.join(__dirname, "images"))); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( 'Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE' ); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); if(req.method==='OPTIONS') { return res.sendStatus(200); } next(); }); app.use( '/graphql', graphqlHttp({ schema: graphqlSchema, rootValue: graphqlResolver, graphiql:true, formatError(err) { if(!err.originalError) { return err; } const data = err.originalError.data; const message = err.message || 'An error occurred.'; const code = err.originalError.code || 500; return { message: message, status: code, data: data }; } }) ); app.use((error, req, res, next) => { console.log(error); const status = error.statusCode || 500; const message = error.message; const data = error.data; res.status(status).json({ message: message, data: data }); }); mongoose .connect( "mongodb+srv://brucegust:M1ch3ll3@brucegust-qhxnz.mongodb.net/messages" ) .then(result => { const server = app.listen(8080); }) .catch(err => console.log(err));
A) EADDRINUSE (back to top...) This was a drag that took a while to remedy. The error was:
Error: listen EADDRinuse; address already in us :::8000
While a search on Google was quick to produce a "killall" command, that didn't do the trick. Instead I had to use Command Prompt and it went like this: First, find all of the ports and see who's doing what:
C:Users/b.gust>netstat -aon TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 376 TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:623 0.0.0.0:0 LISTENING 8572 TCP 0.0.0.0:1801 0.0.0.0:0 LISTENING 4324 TCP 0.0.0.0:2103 0.0.0.0:0 LISTENING 4324 TCP 0.0.0.0:2105 0.0.0.0:0 LISTENING 4324 TCP 0.0.0.0:2107 0.0.0.0:0 LISTENING 4324 TCP 0.0.0.0:2179 0.0.0.0:0 LISTENING 4752 TCP 0.0.0.0:3306 0.0.0.0:0 LISTENING 14260 TCP 0.0.0.0:3307 0.0.0.0:0 LISTENING 16848 TCP 0.0.0.0:5357 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:5985 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 11360 TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 7576 TCP 0.0.0.0:16992 0.0.0.0:0 LISTENING 8572 TCP 0.0.0.0:27019 0.0.0.0:0 LISTENING 1808 TCP 0.0.0.0:47001 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:49664 0.0.0.0:0 LISTENING 792 TCP 0.0.0.0:49665 0.0.0.0:0 LISTENING 1620 TCP 0.0.0.0:49666 0.0.0.0:0 LISTENING 2636 TCP 0.0.0.0:49667 0.0.0.0:0 LISTENING 3884 TCP 0.0.0.0:49668 0.0.0.0:0 LISTENING 884 TCP 0.0.0.0:49669 0.0.0.0:0 LISTENING 4324 TCP 0.0.0.0:49670 0.0.0.0:0 LISTENING 864 TCP 0.0.0.0:49711 0.0.0.0:0 LISTENING 884 TCP 0.0.0.0:49767 0.0.0.0:0 LISTENING 2280 TCP 10.0.75.1:445 10.0.75.2:37476 ESTABLISHED 4 TCP 10.0.75.1:5040 0.0.0.0:0 LISTENING 9320 TCP 127.0.0.1:515 0.0.0.0:0 LISTENING 6592 TCP 127.0.0.1:49781 127.0.0.1:49782 ESTABLISHED 7356 TCP 127.0.0.1:49782 127.0.0.1:49781 ESTABLISHED 7356 TCP 127.0.0.1:49800 127.0.0.1:49801 ESTABLISHED 4192 TCP 127.0.0.1:49801 127.0.0.1:49800 ESTABLISHED 4192 TCP 127.0.0.1:49820 0.0.0.0:0 LISTENING 8572 TCP 127.0.0.1:49950 0.0.0.0:0 LISTENING 15676 TCP 127.0.0.1:49993 0.0.0.0:0 LISTENING 12012 TCP 127.0.0.1:49993 127.0.0.1:49995 ESTABLISHED 12012 TCP 127.0.0.1:49995 127.0.0.1:49993 ESTABLISHED 12320 TCP 127.0.0.1:49996 0.0.0.0:0 LISTENING 12012 TCP 127.0.0.1:49996 127.0.0.1:49999 ESTABLISHED 12012 TCP 127.0.0.1:49999 127.0.0.1:49996 ESTABLISHED 3044 TCP 127.0.0.1:59382 127.0.0.1:59383 ESTABLISHED 8572 TCP 127.0.0.1:59383 127.0.0.1:59382 ESTABLISHED 8572 TCP 172.28.190.97:5040 0.0.0.0:0 LISTENING 9320 TCP 192.168.13.62:139 0.0.0.0:0 LISTENING 4 TCP 192.168.13.62:60418 3.215.109.207:27017 ESTABLISHED 11360 TCP 192.168.13.62:60419 107.23.169.230:27017 ESTABLISHED 11360 TCP 192.168.13.62:60420 3.215.202.137:27017 ESTABLISHED 11360 TCP 192.168.13.62:60486 52.242.211.89:443 ESTABLISHED 4708 TCP 192.168.13.62:60491 52.144.52.89:5721 ESTABLISHED 4184 TCP 192.168.13.62:60495 52.144.52.89:5721 ESTABLISHED 4192 TCP 192.168.13.62:60786 107.21.160.158:443 ESTABLISHED 13704 TCP 192.168.13.62:60798 35.174.127.31:443 ESTABLISHED 13704 TCP 192.168.13.62:61100 35.170.0.145:443 ESTABLISHED 2684 TCP 192.168.13.62:61139 52.144.52.89:5721 ESTABLISHED 7356 TCP 192.168.13.62:61609 192.168.1.12:445 ESTABLISHED 4 TCP 192.168.13.62:61736 69.147.64.33:443 ESTABLISHED 13704 TCP 192.168.13.62:61790 72.21.91.29:80 CLOSE_WAIT 8632 TCP 192.168.13.62:61887 23.15.135.9:443 ESTABLISHED 13704 TCP 192.168.13.62:61894 172.224.187.18:443 ESTABLISHED 13704 TCP 192.168.13.62:61900 23.79.193.133:443 ESTABLISHED 13704 TCP 192.168.13.62:61921 151.101.2.49:443 ESTABLISHED 13704 TCP 192.168.13.62:61925 151.101.184.134:443 ESTABLISHED 13704 TCP 192.168.13.62:61926 192.132.33.46:443 ESTABLISHED 13704 TCP 192.168.13.62:61931 23.79.203.102:443 ESTABLISHED 13704 TCP 192.168.13.62:61932 104.121.94.99:443 ESTABLISHED 13704 TCP 192.168.13.62:61933 23.79.203.102:443 ESTABLISHED 13704 TCP 192.168.13.62:61938 151.101.0.166:443 ESTABLISHED 13704 TCP 192.168.13.62:61942 67.202.110.13:443 ESTABLISHED 13704 TCP 192.168.13.62:61943 104.106.27.115:443 ESTABLISHED 13704 TCP 192.168.13.62:61945 23.79.194.49:443 ESTABLISHED 13704 TCP 192.168.13.62:61956 208.100.17.190:443 ESTABLISHED 13704 TCP 192.168.13.62:61958 151.101.2.2:443 ESTABLISHED 13704 TCP 192.168.13.62:61963 104.24.16.91:443 ESTABLISHED 13704 TCP 192.168.13.62:61977 50.57.31.206:443 ESTABLISHED 13704 TCP 192.168.13.62:61978 151.101.2.49:443 ESTABLISHED 13704 TCP 192.168.13.62:61985 172.224.201.229:443 ESTABLISHED 13704 TCP 192.168.13.62:61995 172.226.186.40:443 ESTABLISHED 13704 TCP 192.168.13.62:62000 172.224.201.229:443 ESTABLISHED 13704 TCP 192.168.13.62:62010 151.101.186.114:443 ESTABLISHED 13704 TCP 192.168.13.62:62014 13.249.122.78:443 ESTABLISHED 13704 TCP 192.168.13.62:62021 23.79.194.49:443 ESTABLISHED 13704 TCP 192.168.13.62:62029 23.79.194.49:443 ESTABLISHED 13704 TCP 192.168.13.62:62032 172.226.179.36:443 ESTABLISHED 13704 TCP 192.168.13.62:62033 23.79.194.49:443 ESTABLISHED 13704 TCP 192.168.13.62:62038 23.79.194.49:443 ESTABLISHED 13704 TCP 192.168.13.62:62040 104.36.113.23:443 ESTABLISHED 13704 TCP 192.168.13.62:62041 172.224.182.34:443 ESTABLISHED 13704 TCP 192.168.13.62:62044 13.249.122.78:443 ESTABLISHED 13704 TCP 192.168.13.62:62046 151.101.186.114:443 ESTABLISHED 13704 TCP 192.168.13.62:62048 104.36.113.34:443 ESTABLISHED 13704 TCP 192.168.13.62:62049 104.36.113.34:443 ESTABLISHED 13704 TCP 192.168.13.62:62055 23.72.233.140:443 ESTABLISHED 13704 TCP 192.168.13.62:62064 23.72.233.140:443 ESTABLISHED 13704 TCP 192.168.13.62:62065 104.36.115.114:443 ESTABLISHED 13704 TCP 192.168.13.62:62066 104.36.115.114:443 ESTABLISHED 13704 TCP 192.168.13.62:62071 162.248.19.147:443 ESTABLISHED 13704 TCP 192.168.13.62:62093 104.84.235.66:443 ESTABLISHED 13704 TCP 192.168.13.62:62096 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62097 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62098 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62100 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62102 104.20.119.107:443 ESTABLISHED 13704 TCP 192.168.13.62:62105 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62106 104.19.198.151:443 ESTABLISHED 13704 TCP 192.168.13.62:62109 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62110 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62111 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62112 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62114 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62115 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62116 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62117 23.73.150.77:443 ESTABLISHED 13704 TCP 192.168.13.62:62118 162.248.19.152:443 ESTABLISHED 13704 TCP 192.168.13.62:62125 104.112.206.159:443 ESTABLISHED 13704 TCP 192.168.13.62:62126 23.79.217.70:443 ESTABLISHED 13704 TCP 192.168.13.62:62141 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62142 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62143 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62144 104.114.162.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62225 151.101.185.108:443 ESTABLISHED 13704 TCP 192.168.13.62:62233 104.122.43.43:443 ESTABLISHED 13704 TCP 192.168.13.62:62242 23.79.210.204:443 ESTABLISHED 13704 TCP 192.168.13.62:62243 23.79.207.44:443 ESTABLISHED 13704 TCP 192.168.13.62:62246 23.79.207.44:443 ESTABLISHED 13704 TCP 192.168.13.62:62259 10.101.1.12:1563 TIME_WAIT 0 TCP 192.168.13.62:62260 13.107.13.88:443 ESTABLISHED 12012 TCP 192.168.13.62:62261 72.21.81.200:443 ESTABLISHED 12012 TCP 192.168.13.62:62262 72.21.81.200:443 ESTABLISHED 12012 TCP 192.168.13.62:62268 72.21.81.200:443 ESTABLISHED 1712 TCP 192.168.13.62:62269 104.20.50.118:443 ESTABLISHED 13704 TCP 192.168.13.62:62270 104.20.50.118:443 TIME_WAIT 0 TCP 192.168.13.62:62271 23.73.177.22:443 ESTABLISHED 13704 TCP 192.168.13.62:62272 192.184.69.178:443 ESTABLISHED 13704 TCP 192.168.13.62:62273 192.184.69.178:443 ESTABLISHED 13704 TCP 192.168.13.62:62274 172.217.0.14:443 ESTABLISHED 13704 TCP 192.168.13.62:62277 31.13.65.1:443 ESTABLISHED 13704 TCP 192.168.13.62:62278 34.208.139.105:443 ESTABLISHED 13704 TCP 192.168.13.62:62279 10.101.1.12:1563 TIME_WAIT 0 TCP 192.168.13.62:62280 205.185.216.10:443 ESTABLISHED 13704 TCP 192.168.13.62:62281 54.175.52.116:443 ESTABLISHED 13704 TCP 192.168.13.62:62282 198.54.12.145:443 ESTABLISHED 13704 TCP 192.168.13.62:62284 198.54.12.97:443 ESTABLISHED 13704 TCP 192.168.13.62:62285 52.73.145.140:443 ESTABLISHED 13704 TCP 192.168.13.62:62287 104.84.238.39:443 ESTABLISHED 13704 TCP 192.168.13.62:62288 199.166.0.26:443 CLOSE_WAIT 13704 TCP 192.168.13.62:62290 52.0.189.80:443 ESTABLISHED 13704 TCP 192.168.13.62:62292 23.111.8.18:443 ESTABLISHED 13704 TCP 192.168.13.62:62293 104.84.238.39:443 ESTABLISHED 13704 TCP 192.168.13.62:62296 173.194.55.92:443 TIME_WAIT 0 TCP 192.168.13.62:62298 13.249.87.117:443 ESTABLISHED 13704 TCP 192.168.13.62:62299 31.13.65.36:443 ESTABLISHED 13704 TCP 192.168.13.62:62300 157.240.18.5:443 ESTABLISHED 13704 TCP 192.168.13.62:62301 35.163.105.237:443 ESTABLISHED 13704 TCP 192.168.13.62:62302 52.21.237.164:443 ESTABLISHED 13704 TCP 192.168.13.62:62303 104.244.36.20:443 ESTABLISHED 13704 TCP 192.168.13.62:62304 204.154.111.128:443 ESTABLISHED 13704 TCP 192.168.13.62:62305 104.244.36.20:443 ESTABLISHED 13704 TCP 192.168.13.62:62306 104.244.36.20:443 ESTABLISHED 13704 TCP 192.168.13.62:62307 72.21.81.200:443 ESTABLISHED 15980 TCP [::]:80 [::]:0 LISTENING 4 TCP [::]:135 [::]:0 LISTENING 376 TCP [::]:445 [::]:0 LISTENING 4 TCP [::]:623 [::]:0 LISTENING 8572 TCP [::]:1801 [::]:0 LISTENING 4324 TCP [::]:2103 [::]:0 LISTENING 4324 TCP [::]:2105 [::]:0 LISTENING 4324 TCP [::]:2107 [::]:0 LISTENING 4324 TCP [::]:2179 [::]:0 LISTENING 4752 TCP [::]:3306 [::]:0 LISTENING 14260 TCP [::]:3307 [::]:0 LISTENING 16848 TCP [::]:5357 [::]:0 LISTENING 4 TCP [::]:5985 [::]:0 LISTENING 4 TCP [::]:8000 [::]:0 LISTENING 11360 TCP [::]:8080 [::]:0 LISTENING 7576 TCP [::]:16992 [::]:0 LISTENING 8572 TCP [::]:47001 [::]:0 LISTENING 4 TCP [::]:49664 [::]:0 LISTENING 792 TCP [::]:49665 [::]:0 LISTENING 1620 TCP [::]:49666 [::]:0 LISTENING 2636 TCP [::]:49667 [::]:0 LISTENING 3884 TCP [::]:49668 [::]:0 LISTENING 884 TCP [::]:49669 [::]:0 LISTENING 4324 TCP [::]:49670 [::]:0 LISTENING 864 TCP [::]:49711 [::]:0 LISTENING 884 TCP [::]:49767 [::]:0 LISTENING 2280 TCP [::1]:49819 [::]:0 LISTENING 7092 UDP 0.0.0.0:53 *:* 7936 UDP 0.0.0.0:68 *:* 1560 UDP 0.0.0.0:123 *:* 1364 UDP 0.0.0.0:3702 *:* 4220 UDP 0.0.0.0:3702 *:* 2120 UDP 0.0.0.0:3702 *:* 4220 UDP 0.0.0.0:3702 *:* 2120 UDP 0.0.0.0:5050 *:* 9320 UDP 0.0.0.0:5353 *:* 12908 UDP 0.0.0.0:5353 *:* 1640 UDP 0.0.0.0:5353 *:* 12908 UDP 0.0.0.0:5353 *:* 12908 UDP 0.0.0.0:5353 *:* 12908 UDP 0.0.0.0:5353 *:* 12908 UDP 0.0.0.0:5355 *:* 1640 UDP 0.0.0.0:51363 *:* 13704 UDP 0.0.0.0:51364 *:* 13704 UDP 0.0.0.0:51610 *:* 7936 UDP 0.0.0.0:51611 *:* 7936 UDP 0.0.0.0:55358 *:* 13704 UDP 0.0.0.0:55359 *:* 13704 UDP 0.0.0.0:55812 *:* 3884 UDP 0.0.0.0:58465 *:* 13704 UDP 0.0.0.0:58724 *:* 13704 UDP 0.0.0.0:60186 *:* 13704 UDP 0.0.0.0:60485 *:* 13704 UDP 0.0.0.0:60653 *:* 13704 UDP 0.0.0.0:61750 *:* 2120 UDP 0.0.0.0:63749 *:* 4220 UDP 0.0.0.0:64063 *:* 13704 UDP 0.0.0.0:64814 *:* 13704 UDP 0.0.0.0:64950 *:* 13704 UDP 10.0.75.1:1900 *:* 3176 UDP 10.0.75.1:55155 *:* 3176 UDP 127.0.0.1:1900 *:* 3176 UDP 127.0.0.1:55157 *:* 3176 UDP 127.0.0.1:55573 *:* 884 UDP 127.0.0.1:56290 *:* 1844 UDP 127.0.0.1:63405 *:* 15676 UDP 172.28.190.97:67 *:* 7936 UDP 172.28.190.97:68 *:* 7936 UDP 172.28.190.97:1900 *:* 3176 UDP 172.28.190.97:55154 *:* 3176 UDP 192.168.13.62:137 *:* 4 UDP 192.168.13.62:138 *:* 4 UDP 192.168.13.62:1900 *:* 3176 UDP 192.168.13.62:55156 *:* 3176 UDP [::]:123 *:* 1364 UDP [::]:3702 *:* 4220 UDP [::]:3702 *:* 2120 UDP [::]:3702 *:* 4220 UDP [::]:3702 *:* 2120 UDP [::]:5353 *:* 1640 UDP [::]:5353 *:* 12908 UDP [::]:5353 *:* 12908 UDP [::]:5355 *:* 1640 UDP [::]:51612 *:* 7936 UDP [::]:61751 *:* 2120 UDP [::]:63750 *:* 4220 UDP [::1]:1900 *:* 3176 UDP [::1]:55153 *:* 3176 UDP [fe80::4093:18ee:570c:cec1%14]:1900 *:* 3176 UDP [fe80::4093:18ee:570c:cec1%14]:55152 *:* 3176 UDP [fe80::a1f9:2642:3221:79db%17]:1900 *:* 3176 UDP [fe80::a1f9:2642:3221:79db%17]:55151 *:* 3176
The culprit is the data that's highlighted. However innocent that may look, that was a toxic dealbreaker that kept the app from running. To kill it, you use the PID number in the context of the following command: C:users/b.gust>Taskkill/ PID 11360 /F Again, "11360" is the PID number. Once you hit <Enter>, life returned to normal. Also, if you want to clear the Command Line screen of multiple lines of requests etc, just type cls Also... I tried the above method and came up short for some reason. One thing that struck me as significant is the number of TIDs that were associated with the port number. I did this, and that did the trick:
C:\Users\b.gust>netstat -ano | findstr :3000 TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 15928 TCP [::]:3000 [::]:0 LISTENING 15928 C:\Users\b.gust>tskill 15928
The first thing you want to understand about Node is that it is a "JavaScript Runtime Environment." In order to understand "runtime," you first have to understand the difference between a compiled program and an interpreted program. When you're working on a computer, you're dealing with a machine that understands one's and zeroes. That's it. So... This is what makes Node distinctive when compared to JQuery in that with JQuery you're uploading a library and that's it because it is intepreted code. A) AWS You can't host Node.js on a shared server because you have to install some system components on the server and shared hosting doesn't allow you to do that