Build application with Node, Express and MongoDB via Wes Bos Learn Node course
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

What is this?

Just you wait folks!

Sample Data

To load sample data, run the following command in your terminal:

npm run sample

If you have previously loaded in this data, you can wipe your database 100% clean with:

npm run blowitallaway

That will populate 16 stores with 3 authors and 41 reviews. The logins for the authors are as follows:

Name Email (login) Password
Wes Bos wes
Debbie Downer debbie
Beau beau

run localhost:7777

To see what themes, fonts, settings, etc... that Wes uses, go to

Make sure that node 7.10 is being used via nvm use 7.10 , otherwise app will not work.

created account @ for hosting mongodb DBs

To reference a value/variable in pug file:

#{dog} ....for text alt=Dog ${dog} ....for attributes have to use es6 JS syntax

To override block header in layout.pug, type block header in index.js (or other template file) to override template header in layout.pug

Note: I'm using tabs to space content, Wes is using two spaces

Anywhere you see app.use -> means using global middleware

To create a 'dump' similar to var dump, put this code into pug file: ex, in _storeForm.pug: pre= h.dump(store)

NOTE: app will not work if running node -v 6.9.5, make sure to first switch to nove -v 7.10 in terminal: nvm use 7.10

run node start

////////// // Steps to creating a new store route: // 1) Create new route via routes/index.js // 2) Create a controller via controllers/storeController.js, exports.getStoreBySlug....this queries database for data and passes the data to be rendered to the template // 3) create a store.pug file // 4) For Google maps config/params, see exports.staticMap in helpers.js (note: MongoDB does lat/long and Google does long/lat). Place this helper in store.pug, img.single__map(src=h.staticMap(store.location.coordinates)) // NOTE: backticks `` are used when generating value for href on the fly // Models only need to be imported once in start.js

///////////////// // Reset lost/forgotten password // /////////////
Multi-step process:

-set a reset token, along with an expiration date, in user if user has email address on file

-email info to user, and if user has proper token and date that is not expired, then they will be able to reset password

-index.js: create post route for /account/forgot -'get' route for /account/reset/:token -'post' route for /account/reset/:token

-authController.js: -create 'forgot' authentication method -import Mongoose -include reference to user model -Send them an email with the token -redirect to login page -create exports.reset method in authController.js -create exports.confirmedPasswords method/middleware -create exports.update method/middleware -require promisify -create setPassword method/middleware

User.js: -add resetPasswordToken: String, -resetPasswordExpires: Date to userSchema

reset.pug: -create reset.pug file with reset form...leave off "action" attribute so page will return to itself

//////////////////// // Sending email with Node.js //////////////////// // authController.js: const mail = require('../handlers/mail');

handlers/mail.js: const nodemailer = require('nodemailer'); const pug = require('pug'); const juice = require('juice'); const htmlToText = require('html-to-text'); const promisify = require('es6-promisify');

authController.js: -add req.flash('success', You have been emailed a password reset link.); to exports.forgot method -add const mail = require('../handlers/mail'); -add
await mail.send({ user, filename: 'password-reset', subject: 'Password Reset', resetURL }); (freemium version) - rather than setting up email with SMTP server like Postmark app for real email, use Mailtrap for dev mode..fakes being a mail server -grab username and mail password from Mailtrap SMTP settings interface and add to variables.env file

transport = way to deal with sending different ways of sending email, w/ SMTP being the most popular

/////////// // Create a relationship between each store and each actual user // //////////

author: { type: mongoose.Schema.ObjectId, ref: 'User', required: 'You must supply an author' }


-add this to exports.createStore method/middleware- = req.user._id;

-create new store, go to database and query user and store collections to see relationship created via "_id": ObjectId

"populate" = bring in info about author and place/embed in store collection

-add to exports.getStoreBySlug to embed info about author into each store collection-

.populate('author'); - do not include sensitive info


const confirmOwner = (store, user) => { if (! { throw Error('You must own a store in order to edit it!'); } };

run this function inside of editStore:

confirmOwner(store, req.user);


store.pug: quick dump of store to see author _id in store collection... pre= h.dump(store)

/////// Create new user account and new cannot edit stores can only be accomplished if the correct user is logged in!

///// Update interface so can only see edit/pencil icon that relates to specific user/store owner

storeCard.pug add:

if user &&

///////////////// // Ajax Rest API ////////////////

Loading sample data:

"sample" script in package.json -> runs script called load-sample-data.js (not part of our app, so need to 'require' env variables + connection to DB, etc..) + "blowitallaway" does the same, but then deletes everything


npm run sample

run blowitallaway ....deletes all data

///////////////////// // JSON endpoints and creating MongoDB indexes // /////////////////

Store.js : -define indexex ...index type of 'text' enables search for stores, etc...can now use '$text' operator on queries -create compound index on 'name' and 'description' fields

Mongo shell examples: { "name" : "text", "description" : "text" } ....already created this in Store.js

  • run 'db.stores.getIndexes()' to see indexes or check in 'compass' app interface Search ex: -db.stores.find( { $text: { $search: "coffee" } } )


router.get('/api/search', catchErrors(storeController.searchStores));


create searchStores method

////////////////// // Create AJAX Search interface // ///////////////

-create typeAhead.js

public/javascripts/delicious-app.js: -import typeAhead from './modules/typeAhead'; -typeAhead( $('.search') );

/////////////////// // Create Geospatial Ajax endpoint // /////////////// // Store.js:

  • storeSchema.index({ location: '2dsphere' });//store metadata about location as Geospatial data to be able to search for stores near lat/long search

Mongo for new index (or use Compass app):



-router.get('/api/stores/near', catchErrors(storeController.mapStores));


-create mapStores method

/////////////////// // Plotting Stores on a Custom Google Map / use own API (for images in map) from own site // ///////////////


  • router.get('/map', storeController.mapPage);


  • exports.mapPage = (req, res) => { res.render('map', { title: 'Map' }); };

  • don't forget to include "photo" field on line 153 in order to see our API/photo for each location appear in maps const stores = await Store.find(q).select('slug name description location photo').limit(10);

public/javascripts/modules directory:

  • create map.js file
  • create makeMap +loadPlaces +mapOptions functions
  • add import { $ } from './bling';


  • makeMap( $('#map') ); -import makeMap from './modules/map';

views directory:

  • create map.pug file

layout.pug: Google maps library is already loaded${process.env.MAP_KEY}&libraries=places

//////////////////////// // Pushing user data to our API // ///////////////////// Tutorial #35


hearts: [ { type: mongoose.Schema.ObjectId, ref: 'Store' } ]


if user .store__action.store__action--heart form.heart(method="POST" action=/api/stores/${store._id}/heart) - const heartStrings = => obj.toString()) - const heartClass = heartStrings.includes(store._id.toString()) ? 'heart__button--hearted' : '' button.heart__button(type="submit" name="heart" class=heartClass) != h.icon('heart')

routes/index.js:'/api/stores/:id/heart', catchErrors(storeController.heartStore));


create exports.heartStore method/middleware

  • const User = mongoose.model('User');


-create new heart.js file to update hearts/favs on the fly w/out page refresh


const heartForms = $$('form.heart'); heartForms.on('submit', ajaxHeart);

import ajaxHeart from './modules/heart';

...don't forget to refresh DB after changes

/////////////////////// // Displaying hearted stores // ////////////////////
// routes/index.js:

router.get('/hearts', storeController.getHearts);


exports.getHearts = async (req, res) => { const stores = await Store.find({ _id: { $in: req.user.hearts } }); res.render('stores', { title: 'Hearted Stores', stores }); };

///////////////////// // Adding a reviews data model // //////////////////


-require new reviews model const reviewController = require('../controllers/reviewController');'/reviews/:id', authController.isLoggedIn, catchErrors(reviewController.addReview) );

models directory:

-create new Review.js model/schema

controllers directory:

create new reviewController.js file + new addReview method/middleware

views/mixins directory:

create new _reviewForm.pug template

views/store.pug: -add _reviewForm mixin

-include mixins/_reviewForm

-if user +reviewForm(store)

start.js: -Import all of our reviews


-re-start database...

////////////////////// // Advanced Relationship Population- Displaying our reviews // ///////////////////


  • find reviews where the stores _id property === reviews store property

storeSchema.virtual('reviews', { ref: 'Review', // what model to link? localField: '_id', // which field on the store? foreignField: 'store' // which field on the review? });

-add default setting to virtual field on line #89

{ toJSON: { virtuals: true }, toOjbect: { virtuals: true } }

models/Review.js: -make sure that when review is queried it's going to automatically populate our author field

function autopopulate(next) { this.populate('author'); next(); }

reviewSchema.pre('find', autopopulate); reviewSchema.pre('findOne', autopopulate);


-display reviews on each store function autopopulate(next) { this.populate('reviews'); next(); }

storeSchema.pre('find', autopopulate); storeSchema.pre('findOne', autopopulate);

views/mixins directory:

create new _review.pug template

views/store.pug: -loop over each review per store

if .reviews each review in .review +review(review)

-include review mixin

include mixins/_review

/////////////////////// // Advanced aggregation // ////////////////////

Get list of top 10 stores based on their avg rating:


router.get('/top', catchErrors(storeController.getTopStores));


exports.getTopStores = async (req, res) => { const stores = await Store.getTopStores(); res.render('topStores', { stores, title:'⭐ Top Stores!'}); }

views directory:

-create topStores.pug template


-create aggregation query to get top stores


-uncomment out these lines

line 10 const Review = require('../models/Review');

line 15 const reviews = JSON.parse(fs.readFileSync(__dirname + '/reviews.json', 'utf-8'));

line 21 await Review.remove();

line 30 await Review.insertMany(reviews);

stop server

-delete all current stores and import new ones

npm run blowitallaway

npm run sample ///import new data

npm start //restart server

-display how many reviews are on each store:


create autopopulate function


if .store__action.store__action--count != h.icon('review') span=

/////////////////////// // Implementing pagination // // ////////////////////


line 11 router.get('/stores/page/:page', catchErrors(storeController.getStores));


modify getStores method to implement pagination

views/mixins directory: -create new _pagination.pug template

views/stores.pug -include _pagination.pug mixin

include mixins/_pagination

+pagination(page, pages, count)

/////////////////// // Deployment // ///////////////

If don't already have a Git repo:

create .gitignore file:


variables.env node_modules/ .DS_Store *.log .idea haters/

Create repo: git init git status git add -A or git add . git commit -m 'app' git push origin master

variable.env file:

If have test database, create 2nd database ..can use MLAB...

If creating your own MongoDB database, must set username and password to prevent getting hacked! By default MongoDB does not come w/a username and Password!

Use for email service

Change port in .env to 80


rename start to 'dev' run npm run dev instead of start whenever need to run app locally

run node start.js to start app

//////// // // Deploy to 'Now' // ////////

In package.json:

rename site/app

"now": { "dotenv": "" }

copy/paste variable.env and create

npm install now -g

run now //From directory to be deployed

/////////////////// // Deploy to Heroku // ////////////////

heroku git:remote -a snacks-n-hangs

heroku addons:create mongolab

Created mongolab-fluffy-96391 as MONGODB_URI

heroku addons:open mongolab

heroku config:get MONGODB_URI

returns: mongodb://