One of the best ways to practice putting all the pieces of a MEN-stack app together is by coding a simple example over and over.
Below, you'll find a step-by-step walkthrough for writing the application from start to finish. Here are a few tips to help maximize takeaways from each repetition:
- Test the functionality of your code at every available opportunity and debug if something isn't working properly. If you wait to debug your code until you've written half of it, you'll find it much more difficult to track down errors.
- Once you've been through the code a time or two, switch to this guide which will provide instructions, but less detail.
- Once you're even more comfortable, try using this guide which will provide instructions, but zero code.
Step 1: Navigate to the parent directory where you want to create your app. Use the express generator to create your app's skeleton:
npx express-generator -e recipe-retriever
cd recipe-retriever
code .
mv app.js server.js
// Change var app = require('../app'); to:
var app = require('../server');
Step 5: Create directories for the model, controller, database (config), and views, then add the corresponding files within each:
mkdir config models controllers views/recipes
touch models/recipe.js controllers/recipes.js config/database.js
npm install
npm install mongoose
Step 7: Split the terminal at the bottom of VS Code to open a second window for monitoring the server. Start the server using nodemon and test it out:
nodemon
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/recipes',
{useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true}
);
const db = mongoose.connection;
db.on('connected', function() {
console.log(`Connected to MongoDB at ${db.host}:${db.port}`);
});
require('./config/database');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var recipeSchema = new Schema({
title: {type: String, required: true},
calories: {type: Number},
mealType: {type: String, enum: ['Snack', 'Breakfast', 'Lunch', 'Dinner', 'Dessert']},
recipeUrl: {type: String},
ingredients: [String]
}, {
timestamps: true
}
);
module.exports = mongoose.model('Recipe', recipeSchema);
mv routes/users.js routes/recipes.js
// Change var userRouter = require('./routes/user'); to:
var recipesRouter = require('./routes/recipes');
// Change app.use('/user', userRouter); to:
app.use('/recipes', recipesRouter);
router.get('/', recipesCtrl.index);
function index(req, res){
Recipe.find({})
.then((recipes) => {
res.render('recipes/index', { recipes })
})
}
touch views/recipes/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel='stylesheet' href='/stylesheets/style.css' />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Recipe Retriever</title>
</head>
<body>
<h2>Recipe List</h2><br>
<table id="recipeList">
<thead>
<tr>
<th>Recipe</th>
<th>Meal<br>Type</th>
<th>Recipe<br>URL</th>
</tr>
</thead>
<tbody>
<% recipes.forEach(function(recipe) { %>
<tr>
<td><%= recipe.title %></td>
<td><%= recipe.mealType %></td>
<td><a href="http://<%= recipe.recipeUrl %>">See Recipe</a></td>
<td><a href="/recipes/<%= recipe._id %>">Details</a></td>
</tr>
<% }) %>
</tbody>
</table>
<a href="/recipes/new">Add Recipe</a>
</body>
</html>
table thead th {
padding: 5px;
border-bottom: 2px solid #424748;
}
table td {
padding: 10px;
text-align: center;
}
#recipeList td:nth-child(2), #recipeList td:nth-child(3){
min-width: 100px;
}
Step 18: Change the default 'localhost:3000' landing page to redirect to 'localhost:3000/recipes'. Do this by changing the route in routes/index.js:
router.get('/', function(req, res) {
res.redirect('/recipes')
});
var express = require('express');
var router = express.Router();
var recipesCtrl = require('../controllers/recipes');
router.get('/new', recipesCtrl.new);
module.exports = router;
var Recipe = require('../models/recipe');
module.exports = {
new: newRecipe
}
function newRecipe(req, res) {
res.render('recipe/new');
}
touch views/recipes/new.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel='stylesheet' href='/stylesheets/style.css' />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Recipe Retriever</title>
</head>
<body>
<h2>Enter new recipe:</h2><br>
<form action="/recipes" method="POST">
<label>Recipe Name:
<input type="text" name="title">
</label><br><br>
<label>Meal Type (select one):
<select name="mealType">
<option value="Snack">Snack</option>
<option value="Breakfast">Breakfast</option>
<option value="Lunch">Lunch</option>
<option value="Dinner">Dinner</option>
<option value="Dessert">Dessert</option>
</select>
</label><br><br>
<label>Calories (per serving):
<input type="text" name="calories">
</label><br><br>
<label>Ingredients (separate each with a comma):
<textarea id="ingredientBox" rows="1" type="text" name="ingredients"></textarea>
</label><br><br>
<label>Link to recipe:
<input id="urlInput" type="text" name="recipeUrl">
</label><br><br>
<button type="submit" class="btn btn-success">Add</button>
</form>
</body>
</html>
#ingredientBox {
width: 250px;
}
#ingredientBox:focus {
height: 100px;
}
#urlInput {
width: 430px;
}
router.post('/', recipesCtrl.create);
module.exports = {
new: newRecipe,
create
}
...
...
...
function create(req, res) {
req.body.ingredients = req.body.ingredients.replace(/\s*,\s*/g, ',');
if (req.body.ingredients) req.body.ingredients = req.body.ingredients.split(',');
Recipe.create(req.body)
.then((recipe) => {
console.log('Added recipe to database:', recipe);
res.redirect('/recipes')
})
}
Step 26: Navigate to the new recipe page, fill out the fields, and hit the 'Add' button. Check to make sure the POST request shows up in the terminal currently running the server:
Step 27: Add a route for the 'Details' button that was just created. Add the following to routes/recipes.js:
router.get('/:id', recipesCtrl.show);
function show(req, res) {
Recipe.findById(req.params.id)
.then((recipe) => {
res.render('recipes/show', { recipe })
})
}
touch views/recipes/show.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel='stylesheet' href='/stylesheets/style.css' />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Recipe Retriever</title>
</head>
<body>
<h2>Recipe Details</h2><br>
<div class="details-bold">Recipe Name:</div>
<div><%= recipe.title %></div>
<div class="details-bold">Meal Type:</div>
<div><%= recipe.mealType %></div>
<div class="details-bold">Calories:</div>
<div><%= recipe.calories %></div>
<div class="details-bold">Ingredients:</div>
<%= recipe.ingredients.map(i => i).join(', ') %>
<div class="details-bold">Recipe Link:</div>
<div><%= recipe.recipeUrl %></div>
<a href="/recipes">Back to Recipes</a>
</body>
</html>
.details-bold {
font-weight: bold;
text-decoration: underline;
}
npm i method-override
let methodOverride = require('method-override');
app.use(methodOverride('_method'));
<tr>
<td><%= recipe.title %></td>
<td><%= recipe.mealType %></td>
<td><a href="http://<%= recipe.recipeUrl %>">See Recipe</a></td>
<td><a href="/recipes/<%= recipe._id %>">Details</a></td>
<!-- Add the following form here: -->
<form action="/recipes/<%= recipe._id %>?_method=DELETE" method="POST">
<td><button type="submit" class="btn btn-danger">X</button></td>
</form>
</tr>
router.delete('/:id', recipesCtrl.delete);
Step 34: Add the corresponding controller to controllers/recipes.js (don't forget to add it to module.exports!):
module.exports = {
new: newRecipe,
create,
index,
show,
// Add this line:
delete: deleteRecipe
}
function deleteRecipe(req, res) {
Recipe.findByIdAndDelete(req.params.id)
.then(res.redirect('/recipes'))
}
<!-- ... -->
<div class="details-bold">Recipe Link:</div>
<div><%= recipe.recipeUrl %></div>
<!-- Add the button here: -->
<form action="/recipes/<%= recipe._id %>/edit">
<button type="submit" class="btn btn-warning">Edit</button>
</form>
<a href="/recipes">Back to Recipes</a>
</body>
router.get('/:id/edit', recipesCtrl.edit);
module.exports = {
new: newRecipe,
create,
index,
show,
delete: deleteRecipe,
// Add this line:
edit
}
function edit(req, res) {
Recipe.findById(req.params.id)
.then((recipe) => {
res.render('recipes/edit', { recipe })
})
}
touch views/recipes/edit.ejs
Step 39: Copy the form over from show.ejs to edit.ejs, but modify it to auto-populate the values of each field with the current record's info:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel='stylesheet' href='/stylesheets/style.css' />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Recipe Retriever</title>
</head>
<body>
<h2>Edit Recipe:</h2><br>
<form action="/recipes/<%= recipe._id %>?_method=PUT" method="POST">
<label>Recipe Name:
<input type="text" name="title" value="<%= recipe.title %>">
</label><br><br>
<label>Meal Type (select one):
<select name="mealType">
<option selected ><%= recipe.mealType %></option>
<option value="Snack">Snack</option>
<option value="Breakfast">Breakfast</option>
<option value="Lunch">Lunch</option>
<option value="Dinner">Dinner</option>
<option value="Dessert">Dessert</option>
</select>
</label><br><br>
<label>Calories (per serving):
<input type="text" name="calories" value="<%= recipe.calories %>">
</label><br><br>
<label>Ingredients (separate each with a comma):
<textarea id="ingredientBox" rows="1" type="text" name="ingredients"><%= recipe.ingredients.map(i => i).join(', ') %></textarea>
</label><br><br>
<label>Link to recipe:
<input id="urlInput" type="text" name="recipeUrl" value="<%= recipe.recipeUrl %>">
</label><br><br>
<button type="submit" class="btn btn-success">Update</button>
</form>
</body>
</html>
router.put('/:id', recipesCtrl.update);
function update(req, res) {
req.body.ingredients = req.body.ingredients.replace(/\s*,\s*/g, ',');
if (req.body.ingredients) req.body.ingredients = req.body.ingredients.split(',');
Recipe.findByIdAndUpdate(req.params.id, req.body, {new: true})
.then((recipe) => {res.redirect(`/recipes/${recipe._id}`)})
}