MEN Stack Relating Data Lab Cookbook Exercise: Referencing Related Data

Introduction

In this lab you’ll create the Cookbook app, a dynamic recipe organization tool tailored for cooking enthusiasts! For this exercise, you’ll be developing a web application that’s all about managing recipes and their ingredients. You’ll practice referencing related data schemas in MongoDB with two models - Recipe and Ingredient. Users will have the ability to perform full CRUD operations on recipes, manage their recipe collections, and explore other users’ recipes for inspiration.

MVP: Your primary task is to implement full CRUD operations for the Recipe model, using referencing to associate it with ingredients. Full CRUD is not necessary for the Ingredient model.

User stories and application planning

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

Data modeling and ERD

In this lab, your task is to create two primary models: Recipe and Ingredient. These models represent the core components of your Cookbook app. The Recipe model will reference the Ingredient model to create a comprehensive list of ingredients for each recipe.

Create the Recipe schema

Create a new file in models called recipe.js. Create a mongoose schema with the following properties:

Property Type Required Description
name String true The name of the recipe.
instructions String false The cooking instructions for the recipe.
owner mongoose.Schema.Types.ObjectId true A reference to the User model.
ingredients [mongoose.Schema.Types.ObjectId] false An array of references to the Ingredient model.

Create the Ingredient schema

Create a new file in models called ingredient.js. Create a mongoose schema with the following properties:

Property Type Required Description
name String true The name of the ingredient.

Referencing Data

In this lab, the Recipe model will reference the Ingredient model using an array of ObjectIds. Each recipe will be associated with a User, thereby creating a relationship between users and their collections of recipes and ingredients.

An ERD (Entity-Relationship Diagram) will help you visualize the relationships between these models:

Cookbook App ERD

In this diagram, you can see how each Recipe is connected to a User and how Ingredients are associated with Recipes. This structure forms the backbone of your Cookbook application.

Define RESTful routes for managing data

Use the following charts as guides when building your RESTful routes.

Recipe routes

Action Route HTTP Verb
Index /recipes GET
New /recipes/new GET
Create /recipes POST
Show /recipes/:recipeId GET
Edit /recipes/:recipeId/edit GET
Update /recipes/:recipeId PUT
Delete /recipes/:recipeId DELETE

Ingredient routes

Action Route HTTP Verb
Index /ingredients GET
New /ingredients/new GET
Create /ingredients POST
Show /ingredients/:ingredientId GET
Edit /ingredients/:ingredientId/edit GET
Update /ingredients/:ingredientId PUT
Delete /ingredients/:ingredientId DELETE

Note: you may not need all of these routes and views in your application

Structure and Setup

1. Build the Controllers

// controllers/recipes.js

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

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

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

module.exports = router;

Repeat this process for the ingredients controller.

2. Use the controllers in server

// server.js

const authController = require('./controllers/auth.js');
const recipesController = require('./controllers/recipes.js');
const ingredientsController = require('./controllers/ingredients.js');
// server.js

// below middleware
app.use('/auth', authController);
app.use('/recipes', recipesController);
app.use('/ingredients', ingredientsController);

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

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

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

// server.js

app.use(passUserToView);
app.use('/auth', authController);
app.use(isSignedIn);
app.use('/recipes', recipesController);
app.use('/ingredients', ingredientsController);

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 recipes, 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:

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 replace the hardcoded links in the homepage starter code with a partial and some simple welcome text.

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

Build the recipe routes and views

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

1. Conceptualize the index route for Recipes

For your landing page, you will use:

Implement the following user story:

AAU, I need a dedicated page to view all my recipes, to easily manage and review what I have created.

2. Define the index route for Recipes

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

3. Render the index template for Recipes

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

Build the new recipe page

Implement the following user story:

AAU, I want to easily find and click on an ‘Add New Recipe’ link, which takes me to a form for adding new recipes.

1. Conceptualize the new recipe route

For your new recipe page, you will use:

2. Define the new recipe route

This route will render a page that displays a form to add a new recipe.

3. Render the new recipe template

Build the create recipe functionality

Implement the following user story:

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

1. Conceptualize the create recipe route

This route will:

2. Define the create recipe route

This route will create new recipes in the user’s collection.

Setting the owner during recipe creation:

When a recipe is created, set the owner field to the current user’s ObjectId. This is usually available in the session data if the user is logged in.

Example:

// In controllers/recipes.js, within the create route

router.post('/', async (req, res) => {
  try {
    const newRecipe = new Recipe(req.body);
    newRecipe.owner = req.session.user._id;
    await newRecipe.save();
    // Redirect to recipe index or show page
  } catch (error) {
    // Handle errors
  }
});

Future access control

To ensure that users can only manage their own recipes, you may choose to implement access control checks. Before allowing edit or delete operations, verify that the recipe’s owner matches the current user’s ID.

// Example of an access control check
if (recipe.owner.equals(req.session.user._id)) {
  // Proceed with edit or delete operation
} else {
  // Redirect or show an error message
}

Add recipes to index page

Implement the following user story:

AAU, I need a dedicated page to view all my recipes, to easily manage and review what I have created.

1. Build the index functionality

Refactor the index route to send all recipes created by a user to the index view:

2. Update the index template

Build the show page

Implement the following user story:

AAU, I want to see the full details of each recipe I create.

1. Conceptualize the show recipe route

This route will:

2. Define the show recipe route

4. Build the show recipe functionality

This route will show the individual recipe from the user’s collection.

5. Render the show recipe template

Build delete route

Implement the following user story:

AAU, I need the ability to edit and delete recipes, allowing me full control over managing my creations.

1. Conceptualize the delete recipe route

This route will:

2. Define the delete recipe route

4. Build the delete recipe functionality

This route will delete the recipe from the user’s collection.

Build the edit recipe page

Implement the following user story:

AAU, I need the ability to edit and delete recipes, allowing me full control over managing my creations.

1. Conceptualize the edit recipe route

For your edit recipe page, you will use:

2. Define the edit recipe route

This route will render a page that displays a form to edit a specific recipe.

3. Render the edit recipe template

Build the update recipe functionality

1. Conceptualize the update recipe route

This route will:

2. Define the update recipe route

3. Build the update recipe functionality

This route will update a specific recipe:

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


Connect Ingredients to Recipes

In this part of the lab, you’ll integrate an Ingredient model into the Recipe creation and management process.

We’ve intentionally provided less guidance in this section to encourage creative problem-solving.

Implement the following user stories:

AAU, I want to be able to add new ingredients to my recipe manager. This way, I can keep track of the different ingredients I use across various recipes. AAU, when adding a new recipe, I want an option to quickly add ingredients that are not already in my list. This will streamline the process of recipe creation, ensuring I can include all necessary ingredients without navigating away from the recipe form.

After you’ve achieved full CRUD functionality for recipes, your next step will be to integrate ingredients. Start by creating a simple list of all ingredients GET /ingredients and a form to add new ones POST /ingredients. Then, focus on allowing users to add these ingredients to their recipes. How and where you choose to make those relationships is up to you.

Ingredient routes and logic

Note: It might not be necessary to develop full RESTful routes for Ingredient.

Instead, consider:

Simplifying recipe and ingredient integration

Handling ingredient duplication

As you progress through this section of the lab, keep in mind the simplicity and functionality of your application. While it might be tempting to create a comprehensive system for managing ingredients, sometimes less is more. Focus on the routes and views that are essential for a smooth user experience.

Congrats! You have reached MVP! 🎉


Add a Community page (Level Up)

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 recipes 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’ recipes

Implement the following user story:

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

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

Test your new community page.