MERN PART 3
Learning Objectives
| Students Will Be Able To: |
|---|
| Follow a Process for Adding Functionality to a MERN-Stack App |
| Define a Component as a Class |
| Organize Non-React Code Into Service & API Modules |
Road Map
- MERN: The Whole Enchilada Part 1 of 3
The Plan - Part 3
In Part 3 we will begin to implement the ever so important user authentication.
Along the way we'll also learn how to define a component as a class instead of a function and preview how we're going to organize non-React code into service & API modules.
3. Add Folders to Organize the Express Server Code
While we're creating folders used to organize our code, let's create some for the Express app that you're familiar with and further seperate our app:
- Since we have alot more potential configuration in a full stack mern app we tend to make a
configfolder to break up the apps config into one place instead of finding configuration. - We will also make a routes folder, instead of stuffing the routes in our controllers folder, how they work won't change at all, we are just making a further distinction between the
routesand theapi functionality. modelsandcontrollersare the same as always.
mkdir config routes models controllersWe'll be using the above folders to organize our server-side code soon enough.
Questions?
4. Implement dotenv Middleware and Create a .env File
As we've done in the past, we need a way to access environment variables and secrets.
We're going to use the familiar dotenv middleware and .env file we used used in Unit 2 and is common in most Express apps.
Install and Mount the dotenv Middleware
First the install if you haven't already done so:
npm i dotenvNow let's add it to server.js if you haven't already done so:
require('dotenv').config();
// Always require and configure near the topCreate the .env File if you haven't done so already
Be sure to touch the .env file in the project root folder:
touch .envNow we're setup to hold secrets such as the database connection string...
5. Install mongoose if you haven't already done so and Connect to a Database
As you'll remember, in Unit 2 we used the Mongoose library to interact with a MongoDB database.
Mongoose is also the go to in MERN-Stack apps.
First, we need to install it as usual:
npm i mongooseCreate the config/database.js Module
Just like we did in Unit 2, we'll use a dedicated Node module to connect to the database:
touch config/database.jsThis code might look familiar:
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGO_URI);
const db = mongoose.connection;
db.on('connected', function () {
console.log(`Connected to ${db.name} at ${db.host}:${db.port}`);
});Add the MONGO_URI to the .env:
For now, let's just connect to our locally running MongoDB engine:
MONGO_URI=mongodb://youknowyoururi/mern-infrastructureBe sure to change the connection string and the database name after starting a new project.
Connect to the Database
We require database.js in server.js in order to connect to the database:
require('dotenv').config();
// Connect to the database
require('./config/database');Be sure to require the config/database module after dotenv.
Looking good:
6. Add an "external" crud-helper.js Module Used for Testing Models, etc.
It's worth the short amount of time it takes to create a module we can load in a Node REPL any time we need to.
The module is not run as part of the Express app - it's "external" to it.
Start by creating the module :
touch crud-helper.jsThere's no models to require at this time, but we'll add them as we build out SEI CAFE:
So we can't use it yet but soon we will be able to like below
// Connect to the database
require('dotenv').config();
require('./config/database');
// Require the Mongoose models
// const User = require('./models/user');
// const Item = require('./models/item');
// const Category = require('./models/category');
// const Order = require('./models/order');
// Local variables will come in handy for holding retrieved documents
let user, item, category, order;
let users, items, categories, orders;This is how we will use the crud-helper.js module in the future:
mern-infrastructure[main*] % node
Welcome to Node.js v15.2.0.
Type ".help" for more information.
> .load crud-helper.js
// Connect to the database
require('dotenv').config();
require('./config/database');
// Require the Mongoose models
// const User = require('./models/user');
// const Item = require('./models/item');
// const Category = require('./models/category');
// const Order = require('./models/order');
// Local variables will come in handy
let user, item, category, order;
let users, items, categories, orders;
{}
> Connected to mern-infrastructure at localhost:27017Type .exit or control + c twice to exit the REPL.
It sure doesn't look like much yet - but it's a start!
We're off and running toward the MERN-Stack!
Fruits Practice Setup
- Add Fruits Controllers
- Add Fruits Routing
- Make Frontend talk to fruits backend
- Watch Video For Tutorial
- Note (we didn't add keys for the li's, you can use the
fruit._idas the key)
/controllers/api/fruits
const Fruit = require('../../models/fruit')
const dataController = {
// Index,
index (req, res, next) {
Fruit.find({}, (err, foundFruits) => {
if (err) {
res.status(400).send({
msg: err.message
})
} else {
res.locals.data.fruits = foundFruits
next()
}
})
},
// Destroy
destroy (req, res, next) {
Fruit.findByIdAndDelete(req.params.id, (err, deletedFruit) => {
if (err) {
res.status(400).send({
msg: err.message
})
} else {
res.locals.data.fruit = deletedFruit
next()
}
})
},
// Update
update (req, res, next) {
req.body.readyToEat = req.body.readyToEat === 'on'
Fruit.findByIdAndUpdate(req.params.id, req.body, { new: true }, (err, updatedFruit) => {
if (err) {
res.status(400).send({
msg: err.message
})
} else {
res.locals.data.fruit = updatedFruit
next()
}
})
},
// Create
create (req, res, next) {
req.body.readyToEat = req.body.readyToEat === 'on'
Fruit.create(req.body, (err, createdFruit) => {
if (err) {
res.status(400).send({
msg: err.message
})
} else {
res.locals.data.fruit = createdFruit
next()
}
})
},
// Edit
// Show
show (req, res, next) {
Fruit.findById(req.params.id, (err, foundFruit) => {
if (err) {
res.status(404).send({
msg: err.message,
output: 'Could not find a fruit with that ID'
})
} else {
res.locals.data.fruit = foundFruit
next()
}
})
}
}
const apiController = {
index (req, res, next) {
res.json(res.locals.data.fruits)
},
show (req, res, next) {
res.json(res.locals.data.fruit)
}
}
module.exports = { dataController, apiController }/routes/api/fruits
const express = require('express')
const router = express.Router()
const { dataController, apiController } = require('../../controllers/api/fruits')
// add routes
// Index /api/fruits
router.get('/', dataController.index, apiController.index)
// Delete /api/fruits/:id
router.delete('/:id', dataController.destroy, apiController.show)
// Update /api/fruits/:id
router.put('/:id', dataController.update, apiController.show)
// Create /api/fruits
router.post('/', dataController.create, apiController.show)
// Show /api/fruits/:id
router.get('/:id', dataController.show, apiController.show)
module.exports = routerserver.js
require('dotenv').config()
require('./config/database');
const express = require('express')
const path = require('path')
const favicon = require('serve-favicon')
const logger = require('morgan')
const PORT = process.env.PORT || 3001
const app = express()
app.use(express.json())// req.body
app.use((req, res, next) => {
res.locals.data = {}
next()
})
app.use(logger('dev'))
app.use(favicon(path.join(__dirname, 'build', 'favicon.ico' )))
app.use(express.static(path.join(__dirname, 'build')))
/*
app.use('/api', routes) <====== Finish code once you got it
*/
app.use('/api/fruits', require('./routes/api/fruits'))
app.get('/api/test', (req, res) => {
res.json({'eureka': 'you have found it'})
})
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'))
})
app.listen(PORT, () => {
console.log(`I am listening on ${PORT}`)
})src/pages/FruitsPage/FruitsPage.js
// FruitPage.js
/*
Create
Read (Index & Show)
Update
Destroy
*/
import { useState, useEffect } from 'react'
export default function FruitsPage (props){
const [fruits, setFruits] = useState([])
const [foundFruit, setFoundFruit] = useState(null)
const [newFruit, setNewFruit] = useState({
name: '',
readyToEat: false,
color: ''
})
// index
const getFruits = async () => {
try {
const response = await fetch('/api/fruits')
const data = await response.json()
setFruits(data)
} catch (error) {
console.error(error)
}
}
// delete
const deleteFruit = async (id) => {
try {
const response = await fetch(`/api/fruits/${id}`, {
method: "DELETE",
headers: {
'Content-Type': 'application/json'
}
})
const data = await response.json()
setFoundFruit(data)
} catch (error) {
console.error(error)
}
}
// update
const updateFruit = async (id, updatedData) => {
try {
const response = await fetch(`/api/fruits/${id}`, {
method: "PUT",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({...updatedData})
})
const data = await response.json()
setFoundFruit(data)
} catch (error) {
console.error(error)
}
}
// create
const createFruit = async () => {
try {
const response = await fetch(`/api/fruits`, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({...newFruit})
})
const data = await response.json()
setFoundFruit(data)
setNewFruit({
name: '',
readyToEat: false,
color: ''
})
} catch (error) {
console.error(error)
}
}
const handleChange = (evt) => {
setNewFruit({...newFruit, [evt.target.name]: evt.target.value})
}
useEffect(()=> {
getFruits()
}, [foundFruit])
return (
<>
{
fruits && fruits.length ? (<ul>
{
fruits.map((fruit) => {
return (
<li key={fruit._id}>
{fruit.name} is {fruit.color} {fruit.readyToEat? 'and its ready to eat' : 'its not ready to eat'}
<br/><button onClick={() => deleteFruit(fruit._id)}>Delete This Fruit</button>
</li>
)
})
}
</ul>): <h1>No Fruits Yet Add One Below</h1>
}
{'Name '}<input value={newFruit.name} onChange={handleChange} name="name"></input><br/>
{'Color '}<input value={newFruit.color} onChange={handleChange} name="color"></input><br/>
{'Ready To Eat '}<input type="checkbox" checked={newFruit.readyToEat} onChange={(evt) => setNewFruit({...newFruit, readyToEat: evt.target.checked })}></input><br/>
<button onClick={() => createFruit() }>Create A New Fruit</button>
{
foundFruit? <div>
<h1>{foundFruit.name}</h1>
<h2>{foundFruit.color}</h2>
<h3>{foundFruit.readyToEat? 'I am ready': 'I am not ready'}</h3>
</div>: <>No Fruit in Found Fruit State</>
}
</>
)
}src/App.js
import './App.css';
import { useState, useEffect } from 'react';
import AuthPage from './pages/AuthPage/AuthPage';
import NewOrderPage from './pages/NewOrderPage/NewOrderPage';
import OrderHistoryPage from './pages/OrderHistoryPage/OrderHistoryPage';
import FruitsPage from './pages/FruitsPage/FruitsPage';
import NavBar from './components/NavBar/NavBar';
import { Routes, Route} from 'react-router-dom'
function App() {
const [state, setState] = useState(null)
const [user, setUser ] = useState(null)
const fetchState = async () => {
try {
const response = await fetch('/api/test')
const data = await response.json()
setState(data)
} catch (error) {
console.error(error)
}
}
useEffect(() => {
fetchState()
}, [])
return (
<main className="App">
{
user ?
<>
<NavBar />
<Routes>
<Route path="/fruits" element={<FruitsPage />} />
<Route path="/orders/new" element={<NewOrderPage />} />
<Route path="/orders" element={<OrderHistoryPage/>} />
</Routes>
</>
:
<AuthPage/>
}
</main>
);
}
export default App;