If you are ready for Part 2
Understanding Express Fundamentals 2 with CRUD, MVC, REST, INDUCES and JSX
Terminal Objectives:
- Understand JSX and jsx-view-engine.
- Set up an Express server.
- Configure Mongoose with MongoDB.
- Create a Mongoose model.
- Build Express routes to handle creating and viewing resources.
Topics Covered:
- JSX and jsx-view-engine.
- Express setup.
- Mongoose configuration.
- Creating a Mongoose model.
- Building Views
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:
- Get into "fruits" directory.
- Open the project in your code editor.
Steps to start
- Go to your terminal find your safe project folder
cd fruits
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
- 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)
- 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
- install
method-override
- 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 thename
,_id
,readyToEat
, andcolor
properties.- By using object destructuring, you're saying: "I want to create four new variables called
name
,_id
,readyToEat
, andcolor
, and I want their values to be taken from the corresponding properties in theprops.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.
Update the Show.jsx to link to the Edit Route and Delete Route
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
- Got to New Page
- Make a fruit
- Check Database
- Edit something about the fruit
- Check Database
- Delete Fruit
- 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`)
})
Connections, Schema, Models, Document and Collections
- The Connection connects your Node.js Server to MongoDB
- Schema ensures the structure of your data that you put into the database
- 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.
- Document is an Instance of the Model and is represented by one Object In a collection
- 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
- Collection ====> Table
- Document ====> Row
- Key on Schema =====> Column