MEN Stack Relating Data Lab Cookbook Exercise: Embedding Related Data

Introduction

In this lab you’ll create the Cookbook app, a dynamic pantry manager for cooking enthusiasts! For this exercise, you’ll be practicing embedding a new data schema within a user model. Users will have the power to perform full CRUD operations to manage their pantry items with ease. They’ll also have the opportunity to explore other users’ pantries, drawing inspiration and sharing their love for cooking.

MVP: Your task is to perform full crud on items in a User’s pantry - an embedded data schema on the user model.

User stories and application planning

Use the following user stories to guide the functionality and features of your application:

Data modeling and ERD

Your task is to create a Pantry, an embedded array within the User model. This array represents the user’s collection of food items. Because the pantry holds foods, the name of our schema will be foodSchema.

Create the Food schema

Each user’s pantry will contain an array of foods, each defined by a simple schema:

Property Type Required
name String true

Begin by defining foodSchema in the user model file using the new mongoose.Schema method. Include the necessary properties for food items.

// user.js

const foodSchema = new mongoose.Schema({
  // YOU DO: Define properties of food schema
});

Embed foodSchema in userSchema

Next, modify userSchema to include the foodSchema. Do this by adding a property named pantry. This property will utilize the foodSchema to represent a list of food items associated with a user.

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
  },
  password: {
    type: String,
    required: true,
  },
  pantry: // YOU DO: embed foodSchema here
});

Take at look at this ERD (Entity-Relationship Diagram) to help you visualize the relationships between these entities, User and foodSchema:

Cookbook App ERD

For this exercise, you can ignore the schemas for recipe and ingredient. Additional sections of this lab explore relating data using referencing.

RESTful routes for managing data

Reference this chart when building your RESTful routes in your controller.

Action Route HTTP Verb
Index ‘/users/:userId/foods’ GET
New ‘/users/:userId/foods/new’ GET
Create ‘/users/:userId/foods’ POST
Show ‘/users/:userId/foods/:itemId’ GET
Edit ‘/users/:userId/foods/:itemId/edit’ GET
Update ‘/users/:userId/foods/:itemId’ PUT
Delete ‘/users/:userId/foods/:itemId’ DELETE

Because this data is embedded in the user model, our routes are structured to reflect that relationship.

Structure and Setup

1. Build the controller

// controllers/foods.js

const express = require('express');
const router = express.Router();

const User = require('../models/user.js');

// router logic will go here - will be built later on in the lab

module.exports = router;

2. Use the controller in server

// server.js

const authController = require('./controllers/auth.js');
const foodsController = require('./controllers/foods.js');
// server.js

app.use('/auth', authController);
app.use('/users/:userId/foods', foodsController);

Add middleware

Now you’ll need to add two middleware functions to your application:

Create a middleware folder in your project root. Inside this folder:

Create a file named is-signed-in.js.This function should:

// middleware/is-signed-in.js

const isSignedIn = (req, res, next) => {
  if (req.session.user) return next();
  res.redirect('/auth/sign-in');
};

module.exports = isSignedIn;

Create a file named pass-user-to-view.js in the same middleware folder. This function should:

// middleware/pass-user-to-view.js

const passUserToView = (req, res, next) => {
  res.locals.user = req.session.user ? req.session.user : null;
  next();
};

module.exports = passUserToView;

Import and include both middleware functions above all of the routes and controllers in server.js..

// server.js
const isSignedIn = require('./middleware/is-signed-in.js');
const passUserToView = require('./middleware/pass-user-to-view.js');

These middleware functions should run before any routes that check for a valid user or require a user to be signed in to view a page.

For this application, users must be signed in to view any of the routes associated with their pantry. Therefore, isSignedIn should come above the foods controller, but not before auth.

app.use(passUserToView);
app.use('/auth', authController);
app.use(isSignedIn);
app.use('/users/:userId/foods', foodsController);

You are now ready to begin building your protected routes.

Build the nav partial

Implement the following user story:

AAU, I want an easy and consistent way to navigate through the site, whether I am signed in or not. I need to quickly access options to sign up, sign in, view my pantry, or sign out, depending on my current status.

Currently the links for “Sign up” or “Sign in” are hardcoded onto the homepage. You will need to refactor and move these links into a nav bar partial for a better user experience.

Create a nav inside _navbar.ejs:

<nav>
  <a href="/">Home</a>
  <% if(user) { %>
  <a href="/users/<%=user._id%>/foods">View my Pantry</a>
  <a href="/auth/sign-out">Sign Out</a>
  <% } else { %>
  <a href="/auth/sign-in">Sign In</a>
  <a href="/auth/sign-up">Sign Up</a>
  <% } %>
</nav>

For the best user experience, you’ll want to include this nav partial at the top of all of your existing templates.

<%- include('./partials/_navbar.ejs') %>

With the nav partial included, you can now replace the hardcoded links on your homepage (views/index.ejs) with a partial and some simple welcome text.

<body>
  <%- include('./partials/_navbar.ejs') %>
  <h1>Welcome to CookBook</h1>
</body>

Build the index route

With the main structure of the application complete, it’s time to start building out the site pages guided by user stories.

1. Conceptualize the index route

For your landing page, you will use:

2. Define the index route

router.get('/', (req, res) => {
  res.render('foods/index.ejs');
});

3. Render the index template

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Pantry</title>
  </head>
  <body>
    <h1>Welcome to my Pantry</h1>
  </body>
</html>

Remember to insert your nav partial at the top of the body to keep the views consistent and create ease of navigation between pages. You’ll want to add this to all your new pages.

Test your new route in the browser to confirm that everything renders correctly (hint: use the route in your nav bar).

Build the new page

Implement the following user story:

AAU, I want to easily find and click on an ‘Add New Item’ link, which takes me to a form for adding new items to my pantry

1. Conceptualize the new route

For your new page, you will use:

2. Define the new route

This route will render a page that displays a form to add a new item to the user’s pantry.

3. Render the new template

Build the create functionality

Implement the following user story:

AAU, after filling out the pantry item form, I want to submit it and receive confirmation that the item has been saved in my pantry.

1. Conceptualize the create route

This route will:

2. Define the create route

This route will create new foods in the embedded pantry array on the user model.

Add pantry items to index page

Implement the following user story:

AAU, I need a dedicated page to view all items in my pantry, to easily manage and review what I have stored.

1. Build the index functionality

Refactor the index route to send all items in a user’s pantry to the index view:

2. Update the index template

Build delete route

Implement the following user story:

AAU, I need the ability to edit and delete items in my pantry, allowing me full control over managing my stored items.

1. Conceptualize the delete route

This route will:

2. Define the delete route

4. Build the delete functionality

This route will delete the food item from the user’s pantry.

Build the edit page

Implement the following user story:

AAU, I need the ability to edit and delete items in my pantry, allowing me full control over managing my stored items.

1. Conceptualize the edit route

For your edit page, you will use:

2. Define the edit route and build edit funtionality

This route will render a page that displays a form to edit a specific food item in the user’s pantry.

3. Render the edit template

Build the update functionality

1. Conceptualize the update route

This route will:

2. Define the update route

3. Build the update functionality

This route will update a specific food item from the user’s pantry:

Now that you have full crud functionality, test each route end to end and check for errors before moving on.

Add a Community page

There are two final user views to reach your final application. Building a community page for users to find each other and gain inspiration from the pantry items of others.

Implement the following user story:

AAU, I am interested in viewing a list of all other app users, to foster a sense of community and interaction within the app.

To meet this requirement you will need a new users controller and views directory for users.

View other users’ pantries

Implement the following user story:

AAU, I want the option to click on another user’s profile and view all the items in their pantry, to exchange ideas or find inspiration for my own pantry.

From the community page, users should be able to click on the names of other users and see their pantry. This requires one final route and view.

Test your new community page.

Congrats! You have reached MVP! 🎉