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:

  1. Since we have alot more potential configuration in a full stack mern app we tend to make a config folder to break up the apps config into one place instead of finding configuration.
  2. 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 routes and the api functionality.
  3. models and controllers are the same as always.
mkdir config routes models controllers

We'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 dotenv

Now let's add it to server.js if you haven't already done so:

require('dotenv').config();
// Always require and configure near the top

Create the .env File if you haven't done so already

Be sure to touch the .env file in the project root folder:

touch .env

Now 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 mongoose

Create 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.js

This 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-infrastructure

Be 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.js

There'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:27017

Type .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

  1. Add Fruits Controllers
  2. Add Fruits Routing
  3. Make Frontend talk to fruits backend
  4. Watch Video For Tutorial
  5. Note (we didn't add keys for the li's, you can use the fruit._id as 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 = router

server.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;