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:
- As a User (AAU), I want to sign up for an account and sign in, so that I can access my personal user profile and manage my recipes.
- 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.
- AAU, I need a dedicated page to view all my recipes, to easily manage and review what I have created.
- AAU, I want to see the full details of each recipe I create.
- AAU, I want to easily find and click on an ‘Add New Recipe’ link, which takes me to a form for adding new recipes to my collection.
- AAU, after filling out the recipe form, I want to submit it and receive confirmation that the recipe has been saved.
- AAU, I need the ability to edit and delete recipes in my collection, allowing me full control over managing my data.
- 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.
- AAU, I am interested in viewing a list of all other app users, to foster a sense of community within the app.
- AAU, I want the option to click on another user’s profile and view all the recipes they have created, to exchange ideas or find inspiration for my own collection.
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.
-
You will be referencing the
Recipeschema within theUserschema, establishing a one-to-many relationship -
You will be referencing the
Ingredientschema within theRecipeschema, establishing a many-to-many relationship
An ERD (Entity-Relationship Diagram) will help you visualize the relationships between these models:

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
- Create files for the
recipesandingredientscontrollers in the controllers directory. - Require all essential modules at the top of each controller file.
- Export your routers for use in
server.js.
// 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
- Import the
recipesandingredientscontrollers inserver.js. - Use middleware to direct incoming requests to the respective controllers.
// 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:
- one that restricts access to logged-in users only
- one that makes the user object available to view templates via
res.locals.
Create a middleware folder in your project root. Inside this folder:
Create a file named is-signed-in.js.
- This function should include logic that checks if
req.session.userexists, and if it does, allows the request to continue on the normal chain by invokingnext() - If a user session does not exist, the user should be redirected to the sign in page.
// 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.
- This function should include logic that assigns
req.session.usertores.locals.user(available in our views). If no user is found, we set it tonull. Then allow the request to continue on the normal chain by invokingnext().
// 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 directory called
partialsinside theviewsdirectory. - Inside partials, create a new file called
_navbar.ejs
Create a nav inside _navbar.ejs:
- This nav should have a
homelink displayed on every page. - Other links should be rendered conditionally based on whether or not a
useris signed in. - If a
useris signed in, they should see links to “View my Recipes” or “Sign Out”. - If a
useris not signed in, they should see links to “Sign In” or “Sign Up”.
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:
GET/recipes
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
- In your
recipescontroller, create a newindexroute to serve as the landing page. Test by visitinglocalhost:3000/recipes. - This route should
res.render()anindex.ejsview, showing all recipes associated with a specific user.
router.get('/', async (req, res) => {
res.render('recipes/index.ejs');
});
3. Render the index template for Recipes
-
Create a new directory called
recipesinside of theviewsdirectory. This will hold the templates for yourrecipespages. -
Create an
index.ejsfile inrecipes. Add a basic HTML boilerplate to this file and include a list to display all 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>
- Test your new route in the browser to confirm that everything renders correctly.
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:
GET/recipes/new
2. Define the new recipe route
This route will render a page that displays a form to add a new recipe.
- In
controllers/recipes.js, create anewroute. - This route should
res.render()anew.ejsview. - Add an “Add New Recipe” link to the
indexpage and point it to your new route.
3. Render the new recipe template
- Create a new template called
new.ejsin theviews/recipesdirectory. - This view should display a form to add a new recipe. Design your form based on the
recipeSchemadefined earlier. - The form action will
POSTto thecreateroute.
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:
POSTto/recipes
2. Define the create recipe route
This route will create new recipes in the user’s collection.
- Look up the user from
req.session - Create a new
Recipeobject usingreq.bodyand assign the user’s_idas the owner. - Save the new
Recipe. - Redirect back to the recipe
indexview. - If any errors, log them and redirect back home
/.
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:
- Look up the current user’s recipes
- Send all recipes to the view via
res.locals - If any errors, log them and redirect back home
/.
2. Update the index template
- Use conditional rendering to display a list of recipes or a message that reads “You have not created any recipes yet.”
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:
GET/recipes/:recipeId
2. Define the show recipe route
- Define the
showroute in therecipescontroller
3. Link each recipe to its show show page to the index template
- Link each individual recipe to the
showpage with/recipes/:recipeId
4. Build the show recipe functionality
This route will show the individual recipe from the user’s collection.
- Look up the recipe using the id supplied by
req.params - This route should
res.render()ashow.ejsview - Send the recipe to the view via
res.locals - If any errors, log them and redirect back home
/.
5. Render the show recipe template
- Create a new template called
show.ejsin theviews/recipesdirectory. - This view should display the user’s full recipe.
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:
DELETEto/recipes/:recipeId- Add
?_method=DELETEto utilize the method override middleware
2. Define the delete recipe route
- Define the
deleteroute in therecipescontroller
3. Add delete link to the index template
- Add a simple form element to each recipe that posts to the
deleteroute - The form should appear as a single
deletebutton in the UI
4. Build the delete recipe functionality
This route will delete the recipe from the user’s collection.
- Look up the recipe using the id supplied by
req.params - Use the
.deleteOne()method to delete the recipe - Redirect to
/recipes - If any errors, log them and redirect back home
/.
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:
GET/recipes/:recipeId/edit
2. Define the edit recipe route
This route will render a page that displays a form to edit a specific recipe.
- In
controllers/recipes.js, create aneditroute. - This route should
res.render()anedit.ejsview. - Find the current recipe from the id supplied by
req.paramsand provide it to the edit form. - Add an edit link to each recipe item rendered on the index page.
- If any errors, log them and redirect back home
/.
3. Render the edit recipe template
- Create a new template called
edit.ejsin theviews/recipesdirectory. - This view should display a form to edit a specific recipe. Design your form based on the
recipeSchema. - Auto-fill the form with the
recipedata supplied from the route. - The form action will
POSTto theupdateroute.
Build the update recipe functionality
1. Conceptualize the update recipe route
This route will:
POSTto/recipes/:recipeId- Add
?_method=PUTto utilize the method override middleware
2. Define the update recipe route
- Define the update route in the
recipescontroller
3. Build the update recipe functionality
This route will update a specific recipe:
- Find the current recipe from the id supplied by
req.params - Use the
.save()method, updating the current recipe to reflect the new form data onreq.body - Redirect back to the
showview - If any errors, log them and redirect back home
/.
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:
-
You might only need an index
GET /ingredientsand a create routePOST /ingredientsfor ingredients. How would you structure these to efficiently manage ingredient data? -
Full views for each ingredient route may not be necessary. Consider combining the index and create form on the same page
ingredients/index.ejs. How can you design this page to be both informative and functional? -
Integrate ingredient functionalities directly within recipe pages, such as allowing users to add new ingredients while creating or editing a recipe.
Simplifying recipe and ingredient integration
-
Consider that you may only need one or two additional routes in your
recipescontroller to relate ingredients to recipes. For instance, a single route could handle adding ingredients directly from the recipe show or edit page. -
If you opt for a separate view for adding ingredients to a recipe, an additional route to render this view might be useful. How would this fit into the overall flow of your application?
-
Consider UI elements like checkboxes, or a select multiple element for adding ingredients to recipes in a form. Consider which is the best approach and why?
-
Remember not to over complicate your routing. Focus on what’s necessary for a good user experience and efficient functionality.
Handling ingredient duplication
-
When adding ingredients to recipes, think about how you might unintentionally allow for duplicate ingredients. What are some strategies to prevent this, either in the view or within your route logic?
-
Would you implement a check in the view to inform users of existing ingredients, or would you handle duplicates in the backend logic during ingredient addition?
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.
- Create a simple
/userscontroller with anindexroute to get all users. - Connect your new controller in
server.js - Create a new directory inside of
viewscalledusers. - Add a new view called
index.ejs - Render a list of all users on the
indexpage. - Add a link to your nav partial that says “Explore Our Community”. Link it to your new community page.
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.
- Create a
showroute in your/userscontroller - Create a
show.ejsin theusersviews directory - Add a link for each user’s
showpage in the rendered community list. - On each user’s
showpage, render a list of that user’s recipes. - This list should be read-only.
Test your new community page.