If you are ready for Part 2

Understanding Express Fundamentals 2 with CRUD, MVC, REST, INDUCES and JSX

Terminal Objectives:

  1. Understand JSX and jsx-view-engine.
  2. Set up an Express server.
  3. Configure Mongoose with MongoDB.
  4. Create a Mongoose model.
  5. Build Express routes to handle creating and viewing resources.

Topics Covered:

  1. JSX and jsx-view-engine.
  2. Express setup.
  3. Mongoose configuration.
  4. Creating a Mongoose model.
  5. Building Views

arthur_node_jsx_diagram_photoshopped

mvc-meme

Instructional Guide

URL HTTP Verb Action Used For Mongoose Method View
/fruits/ GET index Displaying a list of all fruits Fruit.find Index.jsx
/fruits/new GET new Display HTML form for creating a new fruit none New.jsx
/fruits/:id DELETE destroy Delete a specific photo Fruit.findByIdAndRemove or Fruit.findByIdAndDelete none
/fruits/:id PATCH/PUT update Update a specific fruit Fruit.findByIdAndUpdate or Fruit.findOneAndUpdate none
/fruits POST create Create a new fruit Fruit.create none
/fruits/:id/edit GET edit Return an HTML form for editing a fruit Fruit.findOne or Fruit.findById Edit.jsx
/fruits/:id GET show Display a specific fruit Fruit.findOne or Fruit.findById Show.jsx

Note: Only GET Requests render a View In RESTful Routing

Setting back up the environment

Here are step-by-step instructions to set up the development environment:

  1. Get into "fruits" directory.
  2. Open the project in your code editor.

Steps to start

  1. Go to your terminal find your safe project folder
  2. cd fruits
  3. code .

Implementing server.js and configuring the method-override package

Creating Delete Route

Delete route will have backend functionality that deletes fruits from the database

Creating Update Route

Update route will have backend functionality that will update existing fruits with new data

Adding method Override

Browser based forms by default only can make either get or post requests, since we want to make PUT & Delete requests there are 2 possible solutions

  1. Solution 1 use dom manipulation to prevent the form default and manually make a put or delete request via ClientSide JS (we will do this later with React in another lesson)
  2. Intercept a post request on the backend and override it to a DELETE or PUT request

We will use an npm package called method-override to do this today with middleware

Creating Edit Route & Edit.jsx

We will create a JSX template that shows the Edit Form and a route that properly renders it.

Updating Show.jsx to have a delete and edit button

We will add buttons to Show.jsx, one that you can click on and will delete the the fruit and another one that will allow you to link to the edit page for that fruit

Lets Do It

Full server.js to start

require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const jsxEngine = require('jsx-view-engine')
const Fruit = require('./models/fruit')
const PORT = process.env.PORT || 3000

const app = express()

app.use(express.urlencoded({ extended: true }))// build a ssr website
// app.use(express.json()) //build an api
app.set('view engine', 'jsx')
app.engine('jsx', jsxEngine())

mongoose.connect(process.env.MONGO_URI)
mongoose.connection.once('open', () => {
    console.log('connected to mongodb')
})

// INDUCES

// INDEX
// list all fruits
app.get('/fruits', async (req, res) => {
    try {
        const foundFruits = await Fruit.find({})
        res.render('fruits/Index', {
            fruits: foundFruits
        })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
})

// NEW
// show the user a form to fill out to create a fruit
app.get('/fruits/new', (req, res) => {
    res.render('fruits/New')
})
// DELETE
// backend only functionality that is used to delete a fruit

// UPDATE
// backend only functionality that is used to update a fruit

// CREATE
// backend only functionality that is used to create a fruit

app.post('/fruits', async (req, res) => {
    if(req.body.readyToEat === 'on'){
        req.body.readyToEat = true
    } else {
        req.body.readyToEat = false
    }
    try{
        const createdFruit = await Fruit.create(req.body)
        res.redirect(`/fruits/${createdFruit._id}`)
    }catch(error){
        res.status(400).send({message: error.message})
    }
})

// EDIT
// show you a form that lets you edit the fruit

// SHOW
// shows you 1 individual fruit
app.get('/fruits/:id', async (req, res) => {
    try {
        const foundFruit = await Fruit.findOne({_id: req.params.id})
        res.render('fruits/Show', {
            fruit: foundFruit
        })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
})

app.listen(PORT, () =>{
    console.log(`Ayo the Port at ${PORT} is lit`)
})

Delete

app.delete('/fruits/:id', async (req, res) => {
    try{
        await Fruit.findOneAndDelete({'_id': req.params.id})
            .then(() => {
                res.redirect('/fruits')
            })
    }catch(error){
        res.status(400).send({ message: error.message })
    }
})

Update

app.put('/fruits/:id', async (req, res) => {
    if(req.body.readyToEat === 'on'){
        req.body.readyToEat = true;
    } else {
        req.body.readyToEat = false;
    }
    try {
        await Fruit.findOneAndUpdate({'_id': req.params.id}, req.body, { new: true })
            .then(()=>{
                res.redirect(`/fruits/${req.params.id}`)
            })
    } catch(error){
        res.status(400).send({ message: error.message })
    }
})

Method Override

  1. install method-override
  2. add it to the server.js and use it under the express.urlencoded middleware
require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const jsxEngine = require('jsx-view-engine')
const methodOverride = require('method-override')
const Fruit = require('./models/fruit')
const PORT = process.env.PORT || 3000

const app = express()

app.use(express.urlencoded({ extended: true }))// build a ssr website
// app.use(express.json()) //build an api
app.use(methodOverride('_method'))
app.set('view engine', 'jsx')
app.engine('jsx', jsxEngine())

/******** ROUTES and stuff below ******/

The line app.use(methodOverride('_method')) makes it so that the method override will check every request that comes into our app, and see if it uses the _method query paramater, if it does it will override the method of that request to whatever the _method has as it's value.

Edit

First lets make the route

app.get('/fruits/:id/edit', async (req, res) => {
    try {
        const foundFruit = await Fruit.findOne({_id: req.params.id})
        res.render('fruits/Edit', {
            fruit: foundFruit
        })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
})

Now the lets make the JSX file by using the command line to touch a views/fruits/Edit.jsx file in our views folder, then opening it in VS Code:

const React = require('react')

function Edit (props) {
    const { name, _id, readyToEat, color } = props.fruit
    return(
        <div>
            <h1>{name} Edit Page</h1>
            <a href='/fruits'>Go back to Index Page</a>
            <form action={`/fruits/${_id}?_method=PUT`} method="POST">
                Name: <input type="text" name="name" defaultValue={name} /><br/>
                Color: <input type="text" name="color" defaultValue={color}/><br/>
                Is Ready To Eat: {readyToEat? <input type="checkbox" name="readyToEat" defaultChecked />: <input type='checkbox' name="readyToEat"/>}<br/>
                <input type="submit" value="Update Fruit" />
            </form>
        </div>
    )
}

module.exports = Edit

In this line const { name, _id, readyToEat color } = props.fruit we use object destructuring.

Object destructuring in JavaScript is a feature that allows you to extract multiple properties from an object in a single line. The line const { name, _id, readyToEat, color } = props.fruit is using object destructuring to extract the name, _id, readyToEat, and color properties from the props.fruit object.

Here's how it works:

  • props.fruit is an object that contains various properties. In this case, we're interested in the name, _id, readyToEat, and color properties.
  • By using object destructuring, you're saying: "I want to create four new variables called name, _id, readyToEat, and color, and I want their values to be taken from the corresponding properties in the props.fruit object."

This means that after this line of code is executed, you can use name, _id, readyToEat, and color as standalone variables in your code.

Here's an analogy to illustrate object destructuring. Imagine you have a box (the object) full of different fruits (the properties). Object destructuring is like taking the fruits you want out of the box and placing them on your kitchen counter so you can use them directly without having to reach into the box each time.

In terms of code, instead of accessing properties with props.fruit.name, props.fruit._id, props.fruit.readyToEat, and props.fruit.color, you can now use name, _id, readyToEat, and color directly. This makes your code cleaner and more readable.

const React = require('react')

function Show(props){
    return(
        <div>
            <h1>{props.fruit.name}</h1>
            <a href='/fruits'>Go back to Index Page</a>
            <p>
                The {props.fruit.name} is {props.fruit.color} and 
                {props.fruit.readyToEat? 'It is ready to eat': 'It is not ready to eat'}
            </p>
              <form action={`/fruits/${props.fruit._id}?_method=DELETE`} method="POST">
                <input type="submit" value={`Delete this ${props.fruit.name}`}/>
            </form>
            <div>
            <a href={`/fruits/${props.fruit._id}/edit`}><button>{`Edit this ${props.fruit.name}`}</button></a>
            </div>
        </div>
    )
}

module.exports = Show

Test our work

  1. Got to New Page
  2. Make a fruit
  3. Check Database
  4. Edit something about the fruit
  5. Check Database
  6. Delete Fruit
  7. Check Database

Finished Server.js

require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const jsxEngine = require('jsx-view-engine')
const methodOverride = require('method-override')
const Fruit = require('./models/fruit')
const PORT = process.env.PORT || 3000

const app = express()

app.use(express.urlencoded({ extended: true }))// build a ssr website
// app.use(express.json()) //build an api
app.use(methodOverride('_method'))
app.set('view engine', 'jsx')
app.engine('jsx', jsxEngine())

mongoose.connect(process.env.MONGO_URI)
mongoose.connection.once('open', () => {
    console.log('connected to mongodb')
})

// INDUCES

// INDEX
// list all fruits
app.get('/fruits', async (req, res) => {
    try {
        const foundFruits = await Fruit.find({})
        res.render('fruits/Index', {
            fruits: foundFruits
        })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
})

// NEW
// show the user a form to fill out to create a fruit
app.get('/fruits/new', (req, res) => {
    res.render('fruits/New')
})
// DELETE
// backend only functionality that is used to delete a fruit
app.delete('/fruits/:id', async (req, res) => {
    try {
        await Fruit.findOneAndDelete({'_id': req.params.id})
            .then(() => {
                res.redirect('/fruits')
            })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
})

// UPDATE
// backend only functionality that is used to update a fruit
app.put('/fruits/:id', async (req, res) => {
    if(req.body.readyToEat === 'on'){
        req.body.readyToEat = true
    } else {
        req.body.readyToEat = false
    }
    try {
        await Fruit.findOneAndUpdate({ '_id': req.params.id }, 
            req.body, { new: true })
            .then(() => {
                res.redirect(`/fruits/${req.params.id}`)
            })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
} )

// CREATE
// backend only functionality that is used to create a fruit

app.post('/fruits', async (req, res) => {
    if(req.body.readyToEat === 'on'){
        req.body.readyToEat = true
    } else {
        req.body.readyToEat = false
    }
    try{
        const createdFruit = await Fruit.create(req.body)
        res.redirect(`/fruits/${createdFruit._id}`)
    }catch(error){
        res.status(400).send({message: error.message})
    }
})

// EDIT
// show you a form that lets you edit the fruit
app.get('/fruits/:id/edit', async (req, res) => {
    try {
        const foundFruit = await Fruit.findOne({'_id': req.params.id})
        res.render('fruits/Edit', {
           fruit: foundFruit 
        })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
})

// SHOW
// shows you 1 individual fruit
app.get('/fruits/:id', async (req, res) => {
    try {
        const foundFruit = await Fruit.findOne({_id: req.params.id})
        res.render('fruits/Show', {
            fruit: foundFruit
        })
    } catch (error) {
        res.status(400).send({ message: error.message })
    }
})




app.listen(PORT, () =>{
    console.log(`Ayo the Port at ${PORT} is lit`)
})

arthur_node_jsx_diagram_photoshopped

mvc-meme

Connections, Schema, Models, Document and Collections

  1. The Connection connects your Node.js Server to MongoDB
  2. Schema ensures the structure of your data that you put into the database
  3. Models is the class that implements the schema and gives us javascript methods we can use to interact with MongoDb Database, it automatically makes a Collection in the DB, comprised of Documents that pass the Schema.
  4. Document is an Instance of the Model and is represented by one Object In a collection
  5. Collection is a group of documents of the same Model

Where Does It Fit in With MVC

URL HTTP Verb Action Used For Mongoose Method View
/fruits/ GET index Displaying a list of all fruits Fruit.find Index.jsx
/fruits/new GET new Display HTML form for creating a new fruit none New.jsx
/fruits/:id DELETE destroy Delete a specific photo Fruit.findByIdAndRemove or Fruit.findByIdAndDelete none
/fruits/:id PATCH/PUT update Update a specific fruit Fruit.findByIdAndUpdate or Fruit.findOneAndUpdate none
/fruits POST create Create a new fruit Fruit.create none
/fruits/:id/edit GET edit Return an HTML form for editing a fruit Fruit.findOne or Fruit.findById Edit.jsx
/fruits/:id GET show Display a specific fruit Fruit.findOne or Fruit.findById Show.jsx

Note: Only GET Requests render a View In RESTful Routing

Understanding Structure

Connections Schema Models Document Collection Database Cluster
In server.js, or connection file Connect to DB using Connection URL In model file identify the structure of your data Use the schema to create a new Model that can connect to DB with JavaScript methods like .find, create and .findOneAndUpdate An instance of the Model that passes the schema, residing in a collection The group of documents that were added to the database by a specific Model A collection of all your collections, every app should have it's own Database All your databases in your Atlas Account live in your cluster, you only need one cluster and all your databases go in the cluster

For people that know SQL

  1. Collection ====> Table
  2. Document ====> Row
  3. Key on Schema =====> Column