CRUD, MVC, REST, INDUCES and JSX: Remember where we are going

arthur_node_jsx_diagram_photoshopped

mvc-meme

Express Checklist

NOTE: Use your own variable names

Setup For Server Side Rendered Basic Restful Full CRUD APP

  • Make a directory for the project
  • Create a server.js
  • Run npm init -y to initialize a Node project in the directory
  • npm install express
  • npm install jsx-view-engine react react-dom method-override mongoose dotenv
  • Require Express in server.js: const express = require('express')
  • Create an instance of an express app: const app = express()
  • Make your app listen on a certain port: app.listen(3000, () => console.log("Running on port 3000!"))
  • Require all modules and configure all middleware including the view engine urlencoded, locals, methodOverride, jsx-view-engine, static
  • Set up .env and .gitignore in your .env add your MONGO_URI and in .gitignore ignore your node_modules and .env file
  • Set up your models and connect your database
  • Set up your dataController, viewController and routeController
  • Create a public/ folder with a css/ subdirectory within it
  • Create a styles.css file within /public/css and add your styling
  • Host your public/ folder: app.use('/assets', express.static('public'))
  • Include your CSS in the head of your views/layout.hbs file: <link rel="stylesheet" href="/assets/css/styles.css" />
  • Build your views Index, Show, New, Create, Edit
  • Add any additional needed static files to public folder
  • Check for Syntax errors
  • setup package.json for deployment

Part 1

JavaScript Standard Style

js-standard-style

This is a summary of the standard JavaScript rules.

Rules

  • Use 2 spaces for indentation.

    eslint: indent

    function hello (name) {
    console.log('hi', name)
    }
  • Use single quotes for strings except to avoid escaping.

    eslint: quotes

    console.log('hello there')    // ✓ ok
    console.log("hello there")    // ✗ avoid
    console.log(`hello there`)    // ✗ avoid
    
    $("<div class='box'>")        // ✓ ok
    console.log(`hello ${name}`)  // ✓ ok
  • No unused variables.

    eslint: no-unused-vars

    function myFunction () {
    var result = something()   // ✗ avoid
    }
  • Add a space after keywords.

    eslint: keyword-spacing

    if (condition) { ... }   // ✓ ok
    if(condition) { ... }    // ✗ avoid
  • Add a space before a function declaration's parentheses.

    eslint: space-before-function-paren

    function name (arg) { ... }   // ✓ ok
    function name(arg) { ... }    // ✗ avoid
    
    run(function () { ... })      // ✓ ok
    run(function() { ... })       // ✗ avoid
  • Always use === instead of ==.
    Exception: obj == null is allowed to check for null || undefined.

    eslint: eqeqeq

    if (name === 'John')   // ✓ ok
    if (name == 'John')    // ✗ avoid
    if (name !== 'John')   // ✓ ok
    if (name != 'John')    // ✗ avoid
  • Infix operators must be spaced.

    eslint: space-infix-ops

    // ✓ ok
    var x = 2
    var message = 'hello, ' + name + '!'
    // ✗ avoid
    var x=2
    var message = 'hello, '+name+'!'
  • Commas should have a space after them.

    eslint: comma-spacing

    // ✓ ok
    var list = [1, 2, 3, 4]
    function greet (name, options) { ... }
    // ✗ avoid
    var list = [1,2,3,4]
    function greet (name,options) { ... }
  • Keep else statements on the same line as their curly braces.

    eslint: brace-style

    // ✓ ok
    if (condition) {
    // ...
    } else {
    // ...
    }
    // ✗ avoid
    if (condition) {
    // ...
    }
    else {
    // ...
    }
  • For multi-line if statements, use curly braces.

    eslint: curly

    // ✓ ok
    if (options.quiet !== true) console.log('done')
    // ✓ ok
    if (options.quiet !== true) {
    console.log('done')
    }
    // ✗ avoid
    if (options.quiet !== true)
    console.log('done')
  • Always handle the err function parameter.

    eslint: handle-callback-err

    // ✓ ok
    run(function (err) {
    if (err) throw err
    window.alert('done')
    })
    // ✗ avoid
    run(function (err) {
    window.alert('done')
    })
  • Declare browser globals with a /* global */ comment.
    Exceptions are: window, document, and navigator.
    Prevents accidental use of poorly-named browser globals like open, length, event, and name.

    /* global alert, prompt */
    
    alert('hi')
    prompt('ok?')

    Explicitly referencing the function or property on window is okay too, though such code will not run in a Worker which uses self instead of window.

    eslint: no-undef

    window.alert('hi')   // ✓ ok
  • Multiple blank lines not allowed.

    eslint: no-multiple-empty-lines

    // ✓ ok
    var value = 'hello world'
    console.log(value)
    // ✗ avoid
    var value = 'hello world'
    // blank line
    // blank line
    console.log(value)
  • For the ternary operator in a multi-line setting, place ? and : on their own lines.

    eslint: operator-linebreak

    // ✓ ok
    var location = env.development ? 'localhost' : 'www.api.com'
    
    // ✓ ok
    var location = env.development
    ? 'localhost'
    : 'www.api.com'
    
    // ✗ avoid
    var location = env.development ?
    'localhost' :
    'www.api.com'
  • For var declarations, write each declaration in its own statement.

    eslint: one-var

    // ✓ ok
    var silent = true
    var verbose = true
    
    // ✗ avoid
    var silent = true, verbose = true
    
    // ✗ avoid
    var silent = true,
      verbose = true
  • Wrap conditional assignments with additional parentheses. This makes it clear that the expression is intentionally an assignment (=) rather than a typo for equality (===).

    eslint: no-cond-assign

    // ✓ ok
    while ((m = text.match(expr))) {
    // ...
    }
    
    // ✗ avoid
    while (m = text.match(expr)) {
    // ...
    }
  • Add spaces inside single line blocks.

    eslint: block-spacing

    function foo () {return true}    // ✗ avoid
    function foo () { return true }  // ✓ ok
  • Use camelcase when naming variables and functions.

    eslint: camelcase

    function my_function () { }    // ✗ avoid
    function myFunction () { }     // ✓ ok
    
    var my_var = 'hello'           // ✗ avoid
    var myVar = 'hello'            // ✓ ok
  • Trailing commas not allowed.

    eslint: comma-dangle

    var obj = {
      message: 'hello',   // ✗ avoid
    }
  • Commas must be placed at the end of the current line.

    eslint: comma-style

    var obj = {
      foo: 'foo'
      ,bar: 'bar'   // ✗ avoid
    }
    
    var obj = {
      foo: 'foo',
      bar: 'bar'   // ✓ ok
    }
  • Dot should be on the same line as property.

    eslint: dot-location

    console.
      log('hello')  // ✗ avoid
    
    console
      .log('hello') // ✓ ok
  • Files must end with a newline.

    eslint: eol-last

  • No space between function identifiers and their invocations.

    eslint: func-call-spacing

    console.log ('hello') // ✗ avoid
    console.log('hello')  // ✓ ok
  • Add space between colon and value in key value pairs.

    eslint: key-spacing

    var obj = { 'key' : 'value' }    // ✗ avoid
    var obj = { 'key' :'value' }     // ✗ avoid
    var obj = { 'key':'value' }      // ✗ avoid
    var obj = { 'key': 'value' }     // ✓ ok
  • Constructor names must begin with a capital letter.

    eslint: new-cap

    function animal () {}
    var dog = new animal()    // ✗ avoid
    
    function Animal () {}
    var dog = new Animal()    // ✓ ok
  • Constructor with no arguments must be invoked with parentheses.

    eslint: new-parens

    function Animal () {}
    var dog = new Animal    // ✗ avoid
    var dog = new Animal()  // ✓ ok
  • Objects must contain a getter when a setter is defined.

    eslint: accessor-pairs

    var person = {
    set name (value) {    // ✗ avoid
      this._name = value
    }
    }
    
    var person = {
    set name (value) {
      this._name = value
    },
    get name () {         // ✓ ok
      return this._name
    }
    }
  • Constructors of derived classes must call super.

    eslint: constructor-super

    class Dog {
    constructor () {
      super()             // ✗ avoid
      this.legs = 4
    }
    }
    
    class Dog extends Animal {
    constructor () {      // ✗ avoid
      this.legs = 4
    }
    }
    
    class Dog extends Animal {
    constructor () {
      super()             // ✓ ok
      this.legs = 4
    }
    }
  • Use array literals instead of array constructors.

    eslint: no-array-constructor

    var nums = new Array(1, 2, 3)   // ✗ avoid
    var nums = [1, 2, 3]            // ✓ ok
  • Avoid using arguments.callee and arguments.caller.

    eslint: no-caller

    function foo (n) {
    if (n <= 0) return
    
    arguments.callee(n - 1)   // ✗ avoid
    }
    
    function foo (n) {
    if (n <= 0) return
    
    foo(n - 1)                // ✓ ok
    }
  • Avoid modifying variables of class declarations.

    eslint: no-class-assign

    class Dog {}
    Dog = 'Fido'    // ✗ avoid
  • Avoid modifying variables declared using const.

    eslint: no-const-assign

    const score = 100
    score = 125       // ✗ avoid
  • Avoid using constant expressions in conditions (except loops).

    eslint: no-constant-condition

    if (false) {    // ✗ avoid
    // ...
    }
    
    if (x === 0) {  // ✓ ok
    // ...
    }
    
    while (true) {  // ✓ ok
    // ...
    }
  • No control characters in regular expressions.

    eslint: no-control-regex

    var pattern = /\x1f/    // ✗ avoid
    var pattern = /\x20/    // ✓ ok
  • No debugger statements.

    eslint: no-debugger

    function sum (a, b) {
    debugger      // ✗ avoid
    return a + b
    }
  • No delete operator on variables.

    eslint: no-delete-var

    var name
    delete name     // ✗ avoid
  • No duplicate arguments in function definitions.

    eslint: no-dupe-args

    function sum (a, b, a) {  // ✗ avoid
    // ...
    }
    
    function sum (a, b, c) {  // ✓ ok
    // ...
    }
  • No duplicate name in class members.

    eslint: no-dupe-class-members

    class Dog {
    bark () {}
    bark () {}    // ✗ avoid
    }
  • No duplicate keys in object literals.

    eslint: no-dupe-keys

    var user = {
    name: 'Jane Doe',
    name: 'John Doe'    // ✗ avoid
    }
  • No duplicate case labels in switch statements.

    eslint: no-duplicate-case

    switch (id) {
    case 1:
      // ...
    case 1:     // ✗ avoid
    }
  • Use a single import statement per module.

    eslint: no-duplicate-imports

    import { myFunc1 } from 'module'
    import { myFunc2 } from 'module'          // ✗ avoid
    
    import { myFunc1, myFunc2 } from 'module' // ✓ ok
  • No empty character classes in regular expressions.

    eslint: no-empty-character-class

    const myRegex = /^abc[]/      // ✗ avoid
    const myRegex = /^abc[a-z]/   // ✓ ok
  • No empty destructuring patterns.

    eslint: no-empty-pattern

    const { a: {} } = foo         // ✗ avoid
    const { a: { b } } = foo      // ✓ ok
  • No using eval().

    eslint: no-eval

    eval( "var result = user." + propName ) // ✗ avoid
    var result = user[propName]             // ✓ ok
  • No reassigning exceptions in catch clauses.

    eslint: no-ex-assign

    try {
    // ...
    } catch (e) {
    e = 'new value'             // ✗ avoid
    }
    
    try {
    // ...
    } catch (e) {
    const newVal = 'new value'  // ✓ ok
    }
  • No extending native objects.

    eslint: no-extend-native

    Object.prototype.age = 21     // ✗ avoid
  • Avoid unnecessary function binding.

    eslint: no-extra-bind

    const name = function () {
    getName()
    }.bind(user)    // ✗ avoid
    
    const name = function () {
    this.getName()
    }.bind(user)    // ✓ ok
  • Avoid unnecessary boolean casts.

    eslint: no-extra-boolean-cast

    const result = true
    if (!!result) {   // ✗ avoid
    // ...
    }
    
    const result = true
    if (result) {     // ✓ ok
    // ...
    }
  • No unnecessary parentheses around function expressions.

    eslint: no-extra-parens

    const myFunc = (function () { })   // ✗ avoid
    const myFunc = function () { }     // ✓ ok
  • Use break to prevent fallthrough in switch cases.

    eslint: no-fallthrough

    switch (filter) {
    case 1:
      doSomething()    // ✗ avoid
    case 2:
      doSomethingElse()
    }
    
    switch (filter) {
    case 1:
      doSomething()
      break           // ✓ ok
    case 2:
      doSomethingElse()
    }
    
    switch (filter) {
    case 1:
      doSomething()
      // fallthrough  // ✓ ok
    case 2:
      doSomethingElse()
    }
  • No floating decimals.

    eslint: no-floating-decimal

    const discount = .5      // ✗ avoid
    const discount = 0.5     // ✓ ok
  • Avoid reassigning function declarations.

    eslint: no-func-assign

    function myFunc () { }
    myFunc = myOtherFunc    // ✗ avoid
  • No reassigning read-only global variables.

    eslint: no-global-assign

    window = {}     // ✗ avoid
  • No implied eval().

    eslint: no-implied-eval

    setTimeout("alert('Hello world')")                   // ✗ avoid
    setTimeout(function () { alert('Hello world') })     // ✓ ok
  • No function declarations in nested blocks.

    eslint: no-inner-declarations

    if (authenticated) {
    function setAuthUser () {}    // ✗ avoid
    }
  • No invalid regular expression strings in RegExp constructors.

    eslint: no-invalid-regexp

    RegExp('[a-z')    // ✗ avoid
    RegExp('[a-z]')   // ✓ ok
  • No irregular whitespace.

    eslint: no-irregular-whitespace

    function myFunc () /*<NBSP>*/{}   // ✗ avoid
  • No using __iterator__.

    eslint: no-iterator

    Foo.prototype.__iterator__ = function () {}   // ✗ avoid
  • No labels that share a name with an in scope variable.

    eslint: no-label-var

    var score = 100
    function game () {
    score: while (true) {      // ✗ avoid
      score -= 10
      if (score > 0) continue score
      break
    }
    }
  • No label statements.

    eslint: no-labels

    label:
    while (true) {
      break label     // ✗ avoid
    }
  • No unnecessary nested blocks.

    eslint: no-lone-blocks

    function myFunc () {
    {                   // ✗ avoid
      myOtherFunc()
    }
    }
    
    function myFunc () {
    myOtherFunc()       // ✓ ok
    }
  • Avoid mixing spaces and tabs for indentation.

    eslint: no-mixed-spaces-and-tabs

  • Do not use multiple spaces except for indentation.

    eslint: no-multi-spaces

    const id =    1234    // ✗ avoid
    const id = 1234       // ✓ ok
  • No multiline strings.

    eslint: no-multi-str

    const message = 'Hello \
                   world'     // ✗ avoid
  • No new without assigning object to a variable.

    eslint: no-new

    new Character()                     // ✗ avoid
    const character = new Character()   // ✓ ok
  • No using the Function constructor.

    eslint: no-new-func

    var sum = new Function('a', 'b', 'return a + b')    // ✗ avoid
  • No using the Object constructor.

    eslint: no-new-object

    let config = new Object()   // ✗ avoid
  • No using new require.

    eslint: no-new-require

    const myModule = new require('my-module')    // ✗ avoid
  • No using the Symbol constructor.

    eslint: no-new-symbol

    const foo = new Symbol('foo')   // ✗ avoid
  • No using primitive wrapper instances.

    eslint: no-new-wrappers

    const message = new String('hello')   // ✗ avoid
  • No calling global object properties as functions.

    eslint: no-obj-calls

    const math = Math()   // ✗ avoid
  • No octal literals.

    eslint: no-octal

    const octal = 042         // ✗ avoid
    const decimal = 34        // ✓ ok
    const octalString = '042' // ✓ ok
  • No octal escape sequences in string literals.

    eslint: no-octal-escape

    const copyright = 'Copyright \251'  // ✗ avoid
  • Avoid string concatenation when using __dirname and __filename.

    eslint: no-path-concat

    const pathToFile = __dirname + '/app.js'            // ✗ avoid
    const pathToFile = path.join(__dirname, 'app.js')   // ✓ ok
  • Avoid using __proto__. Use getPrototypeOf instead.

    eslint: no-proto

    const foo = obj.__proto__               // ✗ avoid
    const foo = Object.getPrototypeOf(obj)  // ✓ ok
  • No redeclaring variables.

    eslint: no-redeclare

    let name = 'John'
    let name = 'Jane'     // ✗ avoid
    
    let name = 'John'
    name = 'Jane'         // ✓ ok
  • Avoid multiple spaces in regular expression literals.

    eslint: no-regex-spaces

    const regexp = /test   value/   // ✗ avoid
    
    const regexp = /test {3}value/  // ✓ ok
    const regexp = /test value/     // ✓ ok
  • Assignments in return statements must be surrounded by parentheses.

    eslint: no-return-assign

    function sum (a, b) {
    return result = a + b     // ✗ avoid
    }
    
    function sum (a, b) {
    return (result = a + b)   // ✓ ok
    }
  • Avoid assigning a variable to itself

    eslint: no-self-assign

    name = name   // ✗ avoid
  • Avoid comparing a variable to itself.

    eslint: no-self-compare

    if (score === score) {}   // ✗ avoid
  • Avoid using the comma operator.

    eslint: no-sequences

    if (doSomething(), !!test) {}   // ✗ avoid
  • Restricted names should not be shadowed.

    eslint: no-shadow-restricted-names

    let undefined = 'value'     // ✗ avoid
  • Sparse arrays are not allowed.

    eslint: no-sparse-arrays

    let fruits = ['apple',, 'orange']       // ✗ avoid
  • Tabs should not be used

    eslint: no-tabs

  • Regular strings must not contain template literal placeholders.

    eslint: no-template-curly-in-string

    const message = 'Hello ${name}'   // ✗ avoid
    const message = `Hello ${name}`   // ✓ ok
  • super() must be called before using this.

    eslint: no-this-before-super

    class Dog extends Animal {
    constructor () {
      this.legs = 4     // ✗ avoid
      super()
    }
    }
  • Only throw an Error object.

    eslint: no-throw-literal

    throw 'error'               // ✗ avoid
    throw new Error('error')    // ✓ ok
  • Whitespace not allowed at end of line.

    eslint: no-trailing-spaces

  • Initializing to undefined is not allowed.

    eslint: no-undef-init

    let name = undefined    // ✗ avoid
    
    let name
    name = 'value'          // ✓ ok
  • No unmodified conditions of loops.

    eslint: no-unmodified-loop-condition

    for (let i = 0; i < items.length; j++) {...}    // ✗ avoid
    for (let i = 0; i < items.length; i++) {...}    // ✓ ok
  • No ternary operators when simpler alternatives exist.

    eslint: no-unneeded-ternary

    let score = val ? val : 0     // ✗ avoid
    let score = val || 0          // ✓ ok
  • No unreachable code after return, throw, continue, and break statements.

    eslint: no-unreachable

    function doSomething () {
    return true
    console.log('never called')     // ✗ avoid
    }
  • No flow control statements in finally blocks.

    eslint: no-unsafe-finally

    try {
    // ...
    } catch (e) {
    // ...
    } finally {
    return 42     // ✗ avoid
    }
  • The left operand of relational operators must not be negated.

    eslint: no-unsafe-negation

    if (!key in obj) {}       // ✗ avoid
    if (!(key in obj)) {}     // ✓ ok
  • Avoid unnecessary use of .call() and .apply().

    eslint: no-useless-call

    sum.call(null, 1, 2, 3)   // ✗ avoid
  • Avoid using unnecessary computed property keys on objects.

    eslint: no-useless-computed-key

    const user = { ['name']: 'John Doe' }   // ✗ avoid
    const user = { name: 'John Doe' }       // ✓ ok
  • No unnecessary constructor.

    eslint: no-useless-constructor

    class Car {
    constructor () {      // ✗ avoid
    }
    }
  • No unnecessary use of escape.

    eslint: no-useless-escape

    let message = 'Hell\o'  // ✗ avoid
  • Renaming import, export, and destructured assignments to the same name is not allowed.

    eslint: no-useless-rename

    import { config as config } from './config'     // ✗ avoid
    import { config } from './config'               // ✓ ok
  • No whitespace before properties.

    eslint: no-whitespace-before-property

    user .name      // ✗ avoid
    user.name       // ✓ ok
  • No using with statements.

    eslint: no-with

    with (val) {...}    // ✗ avoid
  • Maintain consistency of newlines between object properties.

    eslint: object-property-newline

    const user = {
    name: 'Jane Doe', age: 30,
    username: 'jdoe86'            // ✗ avoid
    }
    
    const user = { name: 'Jane Doe', age: 30, username: 'jdoe86' }    // ✓ ok
    
    const user = {
    name: 'Jane Doe',
    age: 30,
    username: 'jdoe86'
    }                                                                 // ✓ ok
  • No padding within blocks.

    eslint: padded-blocks

    if (user) {
                              // ✗ avoid
    const name = getName()
    
    }
    
    if (user) {
    const name = getName()    // ✓ ok
    }
  • No whitespace between spread operators and their expressions.

    eslint: rest-spread-spacing

    fn(... args)    // ✗ avoid
    fn(...args)     // ✓ ok
  • Semicolons must have a space after and no space before.

    eslint: semi-spacing

    for (let i = 0 ;i < items.length ;i++) {...}    // ✗ avoid
    for (let i = 0; i < items.length; i++) {...}    // ✓ ok
  • Must have a space before blocks.

    eslint: space-before-blocks

    if (admin){...}     // ✗ avoid
    if (admin) {...}    // ✓ ok
  • No spaces inside parentheses.

    eslint: space-in-parens

    getName( name )     // ✗ avoid
    getName(name)       // ✓ ok
  • Unary operators must have a space after.

    eslint: space-unary-ops

    typeof!admin        // ✗ avoid
    typeof !admin        // ✓ ok
  • Use spaces inside comments.

    eslint: spaced-comment

    //comment           // ✗ avoid
    // comment          // ✓ ok
    
    /*comment*/         // ✗ avoid
    /* comment */       // ✓ ok
  • No spacing in template strings.

    eslint: template-curly-spacing

    const message = `Hello, ${ name }`    // ✗ avoid
    const message = `Hello, ${name}`      // ✓ ok
  • Use isNaN() when checking for NaN.

    eslint: use-isnan

    if (price === NaN) { }      // ✗ avoid
    if (isNaN(price)) { }       // ✓ ok
  • typeof must be compared to a valid string.

    eslint: valid-typeof

    typeof name === 'undefimed'     // ✗ avoid
    typeof name === 'undefined'     // ✓ ok
  • Immediately Invoked Function Expressions (IIFEs) must be wrapped.

    eslint: wrap-iife

    const getName = function () { }()     // ✗ avoid
    
    const getName = (function () { }())   // ✓ ok
    const getName = (function () { })()   // ✓ ok
  • The * in yield*expressions must have a space before and after.

    eslint: yield-star-spacing

    yield* increment()    // ✗ avoid
    yield * increment()   // ✓ ok
  • Avoid Yoda conditions.

    eslint: yoda

    if (42 === age) { }    // ✗ avoid
    if (age === 42) { }    // ✓ ok

Semicolons

  • No semicolons. (see: 1, 2, 3)

    eslint: semi

    window.alert('hi')   // ✓ ok
    window.alert('hi');  // ✗ avoid
  • Never start a line with (, [, `, or a handful of other unlikely possibilities.

    This is the only gotcha with omitting semicolons, and standard protects you from this potential issue.

    (The full list is: [, (, `, +, *, /, -, ,, ., but most of these will never appear at the start of a line in real code.)

    eslint: no-unexpected-multiline

    // ✓ ok
    ;(function () {
    window.alert('ok')
    }())
    
    // ✗ avoid
    (function () {
    window.alert('ok')
    }())
    // ✓ ok
    ;[1, 2, 3].forEach(bar)
    
    // ✗ avoid
    [1, 2, 3].forEach(bar)
    // ✓ ok
    ;`hello`.indexOf('o')
    
    // ✗ avoid
    `hello`.indexOf('o')

    Note: If you're often writing code like this, you may be trying to be too clever.

    Clever short-hands are discouraged, in favor of clear and readable expressions, whenever possible.

    Instead of this:

    ;[1, 2, 3].forEach(bar)

    This is strongly preferred:

    var nums = [1, 2, 3]
    nums.forEach(bar)

Helpful reading

And a helpful video:

All popular code minifiers in use today use AST-based minification, so they can handle semicolon-less JavaScript with no issues (since semicolons are not required in JavaScript).

Excerpt from "An Open Letter to JavaScript Leaders Regarding Semicolons":

[Relying on automatic semicolon insertion] is quite safe, and perfectly valid JS that every browser understands. Closure compiler, yuicompressor, packer, and jsmin all can properly minify it. There is no performance impact anywhere.

I am sorry that, instead of educating you, the leaders in this language community have given you lies and fear. That was shameful. I recommend learning how statements in JS are actually terminated (and in which cases they are not terminated), so that you can write code that you find beautiful.

In general, \n ends a statement unless:

  1. The statement has an unclosed paren, array literal, or object literal or ends in some other way that is not a valid way to end a statement. (For instance, ending with . or ,.)
  2. The line is -- or ++ (in which case it will decrement/increment the next token.)
  3. It is a for(), while(), do, if(), or else, and there is no {
  4. The next line starts with [, (, +, *, /, -, ,, ., or some other binary operator that can only be found between two tokens in a single expression.

The first is pretty obvious. Even JSLint is ok with \n chars in JSON and parenthesized constructs, and with var statements that span multiple lines ending in ,.

The second is super weird. I’ve never seen a case (outside of these sorts of conversations) where you’d want to do write i\n++\nj, but, point of fact, that’s parsed as i; ++j, not i++; j.

The third is well understood, if generally despised. if (x)\ny() is equivalent to if (x) { y() }. The construct doesn’t end until it reaches either a block, or a statement.

; is a valid JavaScript statement, so if(x); is equivalent to if(x){} or, “If x, do nothing.” This is more commonly applied to loops where the loop check also is the update function. Unusual, but not unheard of.

The fourth is generally the fud-inducing “oh noes, you need semicolons!” case. But, as it turns out, it’s quite easy to prefix those lines with semicolons if you don’t mean them to be continuations of the previous line. For example, instead of this:

foo();
[1,2,3].forEach(bar);

you could do this:

foo()
;[1,2,3].forEach(bar)

The advantage is that the prefixes are easier to notice, once you are accustomed to never seeing lines starting with ( or [ without semis.

Part 2 A deeper dive into MongoDB & Mongoose APART From Express

Lesson Objectives

  1. Explain what an ODM is
  2. Connect to Mongo via text editor
  3. Create a Schema for a collection
  4. Create a model and save it
  5. find a specific model
  6. update a model already in the database
  7. remove a model already in the database
  8. combine actions
  9. Use helper functions to make your life easier
  10. Extend the abilities of models
  11. Extend the abilities of classes
  12. Create a schema for properties of models that are objects
  13. Reference other models by id
  14. Create actions for models to execute during database actions
  15. Use indexes to speed up searches

Explain what an ODM is

ODM stand for Object Document Model. It translates the documents in Mongo into upgraded JavaScript Objects that have more helpful methods and properties when used in conjunction with express.

Rather than use the Mongo shell to create, read, update and delete documents, we'll use an npm package called mongoose. Mongoose will allow us to create schemas, do validations and make it easier to interact with Mongo inside an express app.

Make a Schema

A schema will allow us to set specific keys in our objects. So if we have a key of name, we won't be able to insert other keys that don't match like firstName or names. This helps keep our data more organized and reduces the chance of errors.

We can also specify the datatypes. We can set the datatype of name to a string, age to a number, dateOfBirth to a Date, bff to a Boolean etc.

We can also make some fields required and we can set default values as well.

Here is a sample Schema, with many options. We'll be making a smaller variation of this

const articleSchema = new Schema(
  {
    title: { type: String, required: true, unique: true }, //can say whether we want properties to be required or unique
    author: { type: String, required: true },
    body: String,
    comments: [{ body: String, commentDate: Date }], // can have arrays of objects with specific properties
    publishDate: { type: Date, default: Date.now }, // can set defaults for properties
    hidden: Boolean,
    meta: {
      // can have properties that are objects
      votes: Number,
      favs: Number,
    },
  },
  { timestamps: true }
);

Basic Set Up

In software_classwork/unit_2

  • mkdir learn_mongoose
  • cd learn_mongoose
  • touch app.js
  • npm init -y and go through the prompts
  • npm i mongoose dotenv
  • touch tweet.js .env
  • code .

Set Up Mongoose

Inside app.js

  • require mongoose
// Dependencies
require('dotenv').config
const mongoose = require("mongoose");
const Tweet = require("./tweet.js");
  • tell Mongoose where to connect with Mongo and have it connect with the sub-database tweets (if it doesn't exist, it will be created)
  • set mongoose.connection to a shorter variable name
// Global configuration
const mongoURI = process.env.MONGO_URI;
const db = mongoose.connection;
  • Connect to mongo
mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true });
  • OPTIONAL provide error/success messages about the connections
// Connection Error/Success
// Define callback functions for various events
db.on("error", (err) => console.log(err.message + " is mongod not running?"));
db.on("open", () => console.log("mongo connected: ", mongoURI));
db.on("close", () => console.log("mongo disconnected"));
  • While the connection is open, we won't have control of our terminal. If we want to regain control, we have to close the connection. Let's set leave the connection open for 5 seconds to demonstrate that the app will hang and then we'll get our close message.

Otherwise we have to press control c. When we run an express app, we typically want to leave the connection open, we don't need to get control of terminal back, we just let the app run.

// Automatically close after 5 seconds
// for demonstration purposes to see that you must use `db.close()` in order to regain control of Terminal tab
setTimeout(() => {
  db.close();
}, 5000);
  • The entire configuration for mongoose:
  • Don't memorize it, just set a bookmark and refer back to this as you need it.
  • note the setTimeout was just to demonstrate what db.close() does, you don't always need it
// Dependencies
require('dotenv').config()
const mongoose = require("mongoose");
const Tweet = require("./tweet.js");

// Global Configuration
const mongoURI = process.env.MONGO_URI;
const db = mongoose.connection;

// Connect to Mongo
mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true });

// Connection Error/Success - optional but can be helpful
// Define callback functions for various events
db.on("error", (err) => console.log(err.message + " is Mongod not running?"));
db.on("open", () => console.log("mongo connected: ", mongoURI));
db.on("close", () => console.log("mongo disconnected"));

Set Up Tweet Schema

In tweet.js

const mongoose = require("mongoose"); // require mongoose
const Schema = mongoose.Schema; // create a shorthand for the mongoose Schema constructor
const model = mongoose.model // shorthand for model function

// create a new Schema
// This will define the shape of the documents in the collection
// https://mongoosejs.com/docs/guide.html
const tweetSchema = new Schema(
  {
    title: String,
    body: String,
    author: String,
    likes: { type: Number, default: 0 },
    sponsored: { type: Boolean, default: false },
  },
  { timestamps: true }
);

// Creating Tweet model : We need to convert our schema into a model-- will be stored in 'tweets' collection.  Mongo does this for you automatically
// Model's are fancy constructors compiled from Schema definitions
// An instance of a model is called a document.
// Models are responsible for creating and reading documents from the underlying MongoDB Database
// from here: https://mongoosejs.com/docs/models.html
const Tweet = model("Tweet", tweetSchema);

//make this exportable to be accessed in `app.js`
module.exports = Tweet;

Create a Document with Mongoose

In app.js

Let's make ourselves an object to insert into our database. When we connect with an express app, our data will be coming in as an object from the browser.

const myFirstTweet = {
  title: "Deep Thoughts",
  body: "Friends, I have been navel-gazing",
  author: "Karolin",
};
Tweet.create(myFirstTweet)
// if database transaction succeeds
.then((tweet) => {
  console.log(tweet)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Let's run this with node app.js

We should see:

created via mongoose

Timestamps, deleted, and likes had default values, a unique _id has been generated

Every time we run node app.js it will run the code, and thus insert this object over and over again. Let's not do that. Let's comment it out.

Let's insert many more tweets

const manyTweets = [
  {
    title: "Deep Thoughts",
    body: "Friends, I have been navel-gazing",
    author: "Karolin",
  },
  {
    title: "Sage Advice",
    body: "Friends, I am vegan and so should you",
    author: "Karolin",
    likes: 20,
  },
  {
    title: "Whole Reality",
    body: "I shall deny friendship to anyone who does not exclusively shop at Whole Foods",
    author: "Karolin",
    likes: 40,
  },
  {
    title: "Organic",
    body: "Friends, I have spent $2300 to be one of the first people to own an organic smartphone",
    author: "Karolin",
    likes: 162,
  },
  {
    title: "Confusion",
    body: "Friends, why do you just respond with the word `dislike`? Surely you mean to click the like button?",
    author: "Karolin",
    likes: -100,
  },
  {
    title: "Vespa",
    body: "Friends, my Vespa has been upgraded to run on old french fry oil. Its top speed is now 11 mph",
    author: "Karolin",
    likes: 2,
  },
  {
    title: "Licensed",
    body: "Friends, I am now officially licensed to teach yogalates. Like this to get 10% off a private lesson",
    author: "Karolin",
    likes: 3,
  },
  {
    title: "Water",
    body: "Friends, I have been collecting rain water so I can indulge in locally sourced raw water. Ask me how",
    author: "Karolin",
  },
];

Let's insert all these tweets:

Tweet.insertMany(manyTweets)
// if database transaction succeeds
.then((tweets) => {
  console.log(tweets)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})
  • node app.js

and let's comment it out so we don't insert duplicates

Find Documents with Mongoose

  • Mongoose has 4 methods for this
  • find - generic
  • findById - finds by ID - great for Show routes!
  • findOne - limits the search to the first document found
  • where - allows you to build queries, we won't cover this today

Let's find all

Tweet.find({})
// if database transaction succeeds
.then((tweets) => {
  console.log(tweets)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Let's limit the fields returned, the second argument allows us to pass a string with the fields we are interested in:

Tweet.find({}, "title body")
// if database transaction succeeds
.then((tweets) => {
  console.log(tweets)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Let's look for a specific tweet:

Tweet.find({ title: "Water" })
// if database transaction succeeds
.then((tweet) => {
  console.log(tweet)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

We can also use advanced query options. Let's find the tweets that have 20 or more likes

Tweet.find({ likes: { $gte: 20 } })
// if database transaction succeeds
.then((tweets) => {
  console.log(tweets)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Delete Documents with Mongoose

We have two copies of our first tweet and a few options to delete it

  • remove() danger! Will remove all instances
  • findOneAndRemove() - this seems like a great choice
  • .findByIdAndRemove()- finds by ID - great for delete routes in an express app!
Tweet.findOneAndRemove({ title: "Deep Thoughts" })
// if database transaction succeeds
.then((tweet) => {
  console.log(tweet)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Update Documents with Mongoose

Finally, we have a few options for updating

  • update() - the most generic one
  • findOneAndUpdate()- Let's us find one and update it
  • findByIdAndUpdate() - Let's us find by ID and update - great for update/put routes in an express app!

If we want to have our updated document returned to us in the callback, we have to set an option of {new: true} as the third argument

Tweet.findOneAndUpdate(
  { title: "Vespa" },
  { sponsored: true },
  { new: true })
// if database transaction succeeds
.then((tweet) => {
  console.log(tweet)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

We'll see the console.logged tweet will have the value of sponsored updated to true. Without {new: true} we would get the original unaltered tweet back.

Intermediate

We can count how many tweets we have with likes greater than 20

Tweet.countDocuments({ likes: { $gte: 20 } })
// if database transaction succeeds
.then((count) => {
  console.log(count)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

We can check out all the things we can do at the Mongoose API docs

Advanced & New!

It has an updated query builder that chains much like jQuery.

Do a search, limit the number of returned queries to 2, sort them by title

Tweet.find({ likes: { $gte: 20 } }, "title -_id")
  .limit(2)
  .sort("title")
  .exec()
// if database transaction succeeds
.then((tweets) => {
  console.log(tweets)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Use helper functions to make your life easier

Mongoose's default find gives you an array of objects. But what if you know you only want one object? These convenience methods just give you one object without the usual array surrounding it.

Article.findById('5757191bce5579b805705900')
// if database transaction succeeds
.then((article) => {
  console.log(article)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})
Article.findOne({ author : 'Matt' })
// if database transaction succeeds
.then((tweet) => {
  console.log(tweet)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})
Article.findByIdAndUpdate(
	'5757191bce5579b805705900', // id of what to update
	{ $set: { author: 'Matthew' } }, // how to update it
	{ new : true })
// if database transaction succeeds
.then((article) => {
  console.log(article)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})
Article.findOneAndUpdate(
	{ author: 'Matt' }, // search criteria of what to update
	{ $set: { author: 'Matthew' } }, // how to update it
	{ new : true }) // tells findOneAndUpdate to return modified article, not the original
// if database transaction succeeds
.then((article) => {
  console.log(article)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})
Article.findByIdAndRemove('5757191bce5579b805705900')
// if database transaction succeeds
.then((article) => {
  console.log(article)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})
Article.findOneAndRemove({ author : 'Matt' })
// if database transaction succeeds
.then((article) => {
  console.log(article)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Extend the abilities of models

You can give all models using a specific schema extra properties and methods when creating that schema

//after initial schema declaration
articleSchema.methods.longTitle = ()=>{
	return this.author + ": " + this.title;
}

//instantiate a model and then call it
const article = new Article();
console.log(article.longTitle());

Extend the abilities of classes

You can create static methods for model constructor functions in their schema

//after initial schema declaration
articleSchema.statics.search = function(name){ //because we use this here, we'll need an old-fashioned function
	//In this situation, it's like .find(), but it searches both title and author
	return this.find({
		$or : [
			{ title: new RegExp(name, 'i') },
			{ author: new RegExp(name, 'i') }
		]
	});
}

//call the static method
Article.search('Some')
// if database transaction succeeds
.then((article) => {
  console.log(article)
})
// if database transaction fails
.catch((error) => {
  console.log(error)
})
// close db connection either way
.finally(() => {
 db.close()
})

Reference other models by id

Sub docs are difficult when it comes to updating, because all duplicates must be updated. But we can reference by ID to get around this.

Single ObjectId reference

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
mongoose.connect('mongodb://urifromyouraccounthere/test');

const articleSchema = new Schema({
	title: { type: String },
	author: { type: Schema.Types.ObjectId, ref: 'Author' } //the author property is just an id of another object
});

const authorSchema = new Schema({
	name: { type: String }
});

const Article = mongoose.model('Article', articleSchema);
const Author = mongoose.model('Author', authorSchema);

const matt = new Author({name: 'Matt'});
matt.save(()=>{
	const article1 = new Article({title:'Awesome Title', author: matt._id});
	article1.save(()=>{
		showAll();
	});
});

const showAll = ()=>{
	Article.find().populate('author').exec((error, article)=>{ //dynamically switch out any ids with the objects they reference
		console.log(article);
		mongoose.connection.close();
	})
};

Arrays of ObjectId references

We can do the same with arrays of ids

const authorSchema = new Schema({
	name: { type: String },
	articles: [{type: Schema.Types.ObjectId, ref: 'Articles'}]
});

const Article = mongoose.model('Article', articleSchema);
const Author = mongoose.model('Author', authorSchema);

const matt = new Author({name: 'Matt'});
let article_id;
matt.save(()=>{
	let article1 = new Article({title:'Awesome Title', author: matt._id});
	article1.save(()=>{
		article_id = article1._id;
		matt.articles.push(article1);
		matt.save(showAll);
	});
});

var showAll = (err, author)=>{
	Author.find().populate('articles').exec((err, authors)=>{ //dynamically switch out any ids with the objects they reference
		console.log(authors);
		mongoose.connection.close();
	});
};

Create actions for models to execute during database actions

We can perform actions before and after database work

articleSchema.pre('save', function(next){ //because we use this here, we'll need an old-fashioned function
	console.log(this);
	console.log('saving to backup database');
	next();
});
articleSchema.post('save', function(next){ //because we use this here, we'll need an old-fashioned function
	console.log('saving complete');
});