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:
express -e cuisine-catalog
cd cuisine-catalog
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/cuisine
touch models/cuisine.js controllers/cuisine.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
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/cuisine',
{useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true}
);
var 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 cuisineSchema = 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('Cuisine', cuisineSchema);
mv routes/users.js routes/cuisine.js
// Change var userRouter = require('./routes/user'); to:
var cuisineRouter = require('./routes/cuisine');
// Change app.use('/user', userRouter); to:
app.use('/cuisine', cuisineRouter);
var express = require('express');
var router = express.Router();
var cuisineCtrl = require('../controllers/cuisine');
router.get('/new', cuisineCtrl.new);
module.exports = router;
var Cuisine = require('../models/cuisine');
module.exports = {
new: newCuisine
}
function newCuisine(req, res) {
res.render('cuisine/new');
}
touch views/cuisine/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>Add Cuisine</title>
</head>
<body>
<h2>Enter new cuisine:</h2><br>
<form action="/cuisine" 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('/', cuisineCtrl.create);
module.exports = {
new: newCuisine,
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(',');
var cuisine = new Cuisine(req.body);
cuisine.save(function(err) {
if (err) return res.render('cuisine/new');
console.log('Added cuisine to database: ' + cuisine);
res.redirect('/cuisine');
});
}
Step 20: Navigate to 'localhost:3000/new' in your browser. 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:
router.get('/', cuisineCtrl.index);
function index(req, res) {
Cuisine.find({}, function(err, cuisine) {
if (err) {
console.log(err);
} else {
res.render('cuisine/index', {title: 'Cuisine List', cuisine});
}
});
}
touch views/cuisine/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>Cuisine List</title>
</head>
<body>
<h2>Cuisine List</h2><br>
<table id="cuisineList">
<thead>
<tr>
<th>Recipe</th>
<th>Meal<br>Type</th>
<th>Recipe<br>URL</th>
</tr>
</thead>
<tbody>
<% cuisine.forEach(function(c) { %>
<tr>
<td><%= c.title %></td>
<td><%= c.mealType %></td>
<td><a href="http://<%= c.recipeUrl %>">See Recipe</a></td>
<td><a href="/cuisine/<%= c._id %>">Details</a></td>
</tr>
<% }) %>
</tbody>
</table>
<a href="/cuisine/new">Add Cuisine</a>
</body>
</html>
table thead th {
padding: 5px;
border-bottom: 2px solid #424748;
}
table td {
padding: 10px;
text-align: center;
}
#cuisineList td:nth-child(2), #cuisineList td:nth-child(3){
min-width: 100px;
}
Step 26: Change the default 'localhost:3000' landing page to redirect to 'localhost:3000/cuisine' now that it is properly displaying all items. Do this by changing the route in routes/index.js:
router.get('/', function(req, res, next) {
res.redirect('cuisine/')
});
Step 27: Add a route for the 'Details' button that was just created. Add the following to routes/cuisine.js:
router.get('/:id', cuisineCtrl.show);
function show(req, res) {
Cuisine.findById(req.params.id, function(err, cuisine){
if (err) {
console.log(err);
} else {
res.render('cuisine/show', {title: 'Cuisine Details', cuisine});
}
});
}
touch views/cuisine/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>Cuisine Details</title>
</head>
<body>
<h2>Cuisine Details</h2><br>
<div class="details-bold">Recipe Name:</div>
<div><%= cuisine.title %></div>
<div class="details-bold">Meal Type:</div>
<div><%= cuisine.mealType %></div>
<div class="details-bold">Calories:</div>
<div><%= cuisine.calories %></div>
<div class="details-bold">Ingredients:</div>
<%= cuisine.ingredients.map(i => i).join(', ') %>
<div class="details-bold">Recipe Link:</div>
<div><%= cuisine.recipeUrl %></div>
</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><%= c.title %></td>
<td><%= c.mealType %></td>
<td><a href="http://<%= c.recipeUrl %>">See Recipe</a></td>
<td><a href="/cuisine/<%= c._id %>">Details</a></td>
<!-- Add the following form here: -->
<form action="/cuisine/<%= c._id %>?_method=DELETE" method="POST">
<td><button type="submit" class="btn btn-danger">X</button></td>
</form>
</tr>
router.delete('/:id', cuisineCtrl.delete);
function deleteOne(req, res) {
Cuisine.findByIdAndDelete(req.params.id, function(err, cuisine){
if (err) {
console.log(err);
} else {
console.log('deleting: ' + cuisine);
}
})
res.redirect('/cuisine')
}
<!-- ... -->
<div class="details-bold">Recipe Link:</div>
<div><%= cuisine.recipeUrl %></div>
<!-- Add the button here: -->
<form action="/cuisine/update/<%= cuisine._id %>?_method=PUT">
<button type="submit" class="btn btn-warning">Edit</button>
</form>
</body>
router.put('/update/:id', cuisineCtrl.showUpdate);
function showUpdate(req, res) {
Cuisine.findById(req.params.id, function(err, cuisine) {
if (err) {
console.log(err);
} else {
res.render('cuisine/update', {title: 'Update Cuisine', cuisine});
}
});
}
touch views/cuisine/update.ejs
Step 39: Copy the form over from show.ejs to update.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>Add Cuisine</title>
</head>
<body>
<h2>Update cuisine:</h2><br>
<form action="/cuisine/update/<%= cuisine._id %>" method="POST">
<label>Recipe Name:
<input type="text" name="title" value="<%= cuisine.title %>">
</label><br><br>
<label>Meal Type (select one):
<select name="mealType">
<option selected ><%= cuisine.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="<%= cuisine.calories %>">
</label><br><br>
<label>Ingredients (separate each with a comma):
<textarea id="ingredientBox" rows="1" type="text" name="ingredients"><%= cuisine.ingredients.map(i => i).join(', ') %></textarea>
</label><br><br>
<label>Link to recipe:
<input id="urlInput" type="text" name="recipeUrl" value="<%= cuisine.recipeUrl %>">
</label><br><br>
<button type="submit" class="btn btn-success">Update</button>
</form>
</body>
</html>
router.post('/update/:id', cuisineCtrl.update);
function update(req, res) {
console.log(req.body);
req.body.ingredients = req.body.ingredients.replace(/\s*,\s*/g, ',');
if (req.body.ingredients) req.body.ingredients = req.body.ingredients.split(',');
Cuisine.findByIdAndUpdate(req.params.id,
{
title: req.body.title,
calories: req.body.calories,
mealType: req.body.mealType,
recipeUrl: req.body.recipeUrl,
ingredients: req.body.ingredients
},
{new: true},
function(err, response) {
if (err) {
console.log(err);
} else {
res.redirect('/cuisine/')
}
})
}