LIBRARY
   app.js

    var createError = require('http-errors');
    var express = require('express');
    var path = require('path');
    var cookieParser = require('cookie-parser');
    var logger = require('morgan');

    var indexRouter = require('./routes/index');
    var usersRouter = require('./routes/users');
    var catalogRouter = require('./routes/catalog');  //Import routes for "catalog" area of site

    var compression = require('compression');
    var helmet = require('helmet');

    var app = express();


    // Set up mongoose connection
    var mongoose = require('mongoose');
    var dev_db_url = 'mongodb+srv://cooluser:coolpassword@cluster0-mbdj7.mongodb.net/local_library?retryWrites=true'
    var mongoDB = process.env.MONGODB_URI || dev_db_url;
    mongoose.connect(mongoDB, { useNewUrlParser: true });
    mongoose.Promise = global.Promise;
    var db = mongoose.connection;
    db.on('error', console.error.bind(console, 'MongoDB connection error:'));


    // view engine setup
    app.set('views', path.join(__dirname, 'views'));
    app.set('view engine', 'pug');

    app.use(logger('dev'));
    app.use(express.json());
    app.use(express.urlencoded({ extended: false }));
    app.use(cookieParser());
    app.use(helmet());
    app.use(compression()); // Compress all routes

    app.use(express.static(path.join(__dirname, 'public')));

    app.use('/', indexRouter);
    app.use('/users', usersRouter);
    app.use('/catalog', catalogRouter);  // Add catalog routes to middleware chain.

    // catch 404 and forward to error handler
    app.use(function(req, res, next) {
      next(createError(404));
    });

    // error handler
    app.use(function(err, req, res, next) {
      // set locals, only providing error in development
      res.locals.message = err.message;
      res.locals.error = req.app.get('env') === 'development' ? err : {};

      // render the error page
      res.status(err.status || 500);
      res.render('error');
    });

    module.exports = app;
    
   controllers
   +---- authorController.js

    var Author = require('../models/author')
    var async = require('async')
    var Book = require('../models/book')

    const { body, validationResult } = require('express-validator/check');
    const { sanitizeBody } = require('express-validator/filter');

    // Display list of all Authors.
    exports.author_list = function (req, res, next) {

        Author.find()
            .sort([['family_name', 'ascending']])
            .exec(function (err, list_authors) {
                if (err) { return next(err); }
                // Successful, so render.
                res.render('author_list', { title: 'Author List', author_list: list_authors });
            })

    };

    // Display detail page for a specific Author.
    exports.author_detail = function (req, res, next) {

        async.parallel({
            author: function (callback) {
                Author.findById(req.params.id)
                    .exec(callback)
            },
            authors_books: function (callback) {
                Book.find({ 'author': req.params.id }, 'title summary')
                    .exec(callback)
            },
        }, function (err, results) {
            if (err) { return next(err); } // Error in API usage.
            if (results.author == null) { // No results.
                var err = new Error('Author not found');
                err.status = 404;
                return next(err);
            }
            // Successful, so render.
            res.render('author_detail', { title: 'Author Detail', author: results.author, author_books: results.authors_books });
        });

    };

    // Display Author create form on GET.
    exports.author_create_get = function (req, res, next) {
        res.render('author_form', { title: 'Create Author' });
    };

    // Handle Author create on POST.
    exports.author_create_post = [

        // Validate fields.
        body('first_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.')
            .isAlphanumeric().withMessage('First name has non-alphanumeric characters.'),
        body('family_name').isLength({ min: 1 }).trim().withMessage('Family name must be specified.')
            .isAlphanumeric().withMessage('Family name has non-alphanumeric characters.'),
        body('date_of_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601(),
        body('date_of_death', 'Invalid date of death').optional({ checkFalsy: true }).isISO8601(),

        // Sanitize fields.
        sanitizeBody('first_name').escape(),
        sanitizeBody('family_name').escape(),
        sanitizeBody('date_of_birth').toDate(),
        sanitizeBody('date_of_death').toDate(),

        // Process request after validation and sanitization.
        (req, res, next) => {

            // Extract the validation errors from a request.
            const errors = validationResult(req);

            // Create Author object with escaped and trimmed data
            var author = new Author(
                {
                    first_name: req.body.first_name,
                    family_name: req.body.family_name,
                    date_of_birth: req.body.date_of_birth,
                    date_of_death: req.body.date_of_death,
                }
            );

            if (!errors.isEmpty()) {
                // There are errors. Render form again with sanitized values/errors messages.
                res.render('author_form', { title: 'Create Author', author: author, errors: errors.array() });
                return;
            }
            else {
                // Data from form is valid.

                // Save author.
                author.save(function (err) {
                    if (err) { return next(err); }
                    // Successful - redirect to new author record.
                    res.redirect(author.url);
                });
            }
        }
    ];



    // Display Author delete form on GET.
    exports.author_delete_get = function (req, res, next) {

        async.parallel({
            author: function (callback) {
                Author.findById(req.params.id).exec(callback)
            },
            authors_books: function (callback) {
                Book.find({ 'author': req.params.id }).exec(callback)
            },
        }, function (err, results) {
            if (err) { return next(err); }
            if (results.author == null) { // No results.
                res.redirect('/catalog/authors');
            }
            // Successful, so render.
            res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books });
        });

    };

    // Handle Author delete on POST.
    exports.author_delete_post = function (req, res, next) {

        async.parallel({
            author: function (callback) {
                Author.findById(req.body.authorid).exec(callback)
            },
            authors_books: function (callback) {
                Book.find({ 'author': req.body.authorid }).exec(callback)
            },
        }, function (err, results) {
            if (err) { return next(err); }
            // Success.
            if (results.authors_books.length > 0) {
                // Author has books. Render in same way as for GET route.
                res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books });
                return;
            }
            else {
                // Author has no books. Delete object and redirect to the list of authors.
                Author.findByIdAndRemove(req.body.authorid, function deleteAuthor(err) {
                    if (err) { return next(err); }
                    // Success - go to author list.
                    res.redirect('/catalog/authors')
                })

            }
        });

    };

    // Display Author update form on GET.
    exports.author_update_get = function (req, res, next) {

        Author.findById(req.params.id, function (err, author) {
            if (err) { return next(err); }
            if (author == null) { // No results.
                var err = new Error('Author not found');
                err.status = 404;
                return next(err);
            }
            // Success.
            res.render('author_form', { title: 'Update Author', author: author });

        });
    };

    // Handle Author update on POST.
    exports.author_update_post = [

        // Validate fields.
        body('first_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.')
            .isAlphanumeric().withMessage('First name has non-alphanumeric characters.'),
        body('family_name').isLength({ min: 1 }).trim().withMessage('Family name must be specified.')
            .isAlphanumeric().withMessage('Family name has non-alphanumeric characters.'),
        body('date_of_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601(),
        body('date_of_death', 'Invalid date of death').optional({ checkFalsy: true }).isISO8601(),

        // Sanitize fields.
        sanitizeBody('first_name').escape(),
        sanitizeBody('family_name').escape(),
        sanitizeBody('date_of_birth').toDate(),
        sanitizeBody('date_of_death').toDate(),

        // Process request after validation and sanitization.
        (req, res, next) => {

            // Extract the validation errors from a request.
            const errors = validationResult(req);

            // Create Author object with escaped and trimmed data (and the old id!)
            var author = new Author(
                {
                    first_name: req.body.first_name,
                    family_name: req.body.family_name,
                    date_of_birth: req.body.date_of_birth,
                    date_of_death: req.body.date_of_death,
                    _id: req.params.id
                }
            );

            if (!errors.isEmpty()) {
                // There are errors. Render the form again with sanitized values and error messages.
                res.render('author_form', { title: 'Update Author', author: author, errors: errors.array() });
                return;
            }
            else {
                // Data from form is valid. Update the record.
                Author.findByIdAndUpdate(req.params.id, author, {}, function (err, theauthor) {
                    if (err) { return next(err); }
                    // Successful - redirect to genre detail page.
                    res.redirect(theauthor.url);
                });
            }
        }
    ];
    
   +---- bookController.js

    var Book = require('../models/book');
    var Author = require('../models/author');
    var Genre = require('../models/genre');
    var BookInstance = require('../models/bookinstance');

    const { body,validationResult } = require('express-validator/check');
    const { sanitizeBody } = require('express-validator/filter');

    var async = require('async');

    exports.index = function(req, res) {

        async.parallel({
            book_count: function(callback) {
                Book.count(callback);
            },
            book_instance_count: function(callback) {
                BookInstance.count(callback);
            },
            book_instance_available_count: function(callback) {
                BookInstance.count({status:'Available'},callback);
            },
            author_count: function(callback) {
                Author.count(callback);
            },
            genre_count: function(callback) {
                Genre.count(callback);
            },
        }, function(err, results) {
            res.render('index', { title: 'Local Library Home', error: err, data: results });
        });
    };


    // Display list of all books.
    exports.book_list = function(req, res, next) {

      Book.find({}, 'title author ')
        .populate('author')
        .exec(function (err, list_books) {
          if (err) { return next(err); }
          // Successful, so render
          res.render('book_list', { title: 'Book List', book_list:  list_books});
        });

    };

    // Display detail page for a specific book.
    exports.book_detail = function(req, res, next) {

        async.parallel({
            book: function(callback) {

                Book.findById(req.params.id)
                  .populate('author')
                  .populate('genre')
                  .exec(callback);
            },
            book_instance: function(callback) {

              BookInstance.find({ 'book': req.params.id })
              .exec(callback);
            },
        }, function(err, results) {
            if (err) { return next(err); }
            if (results.book==null) { // No results.
                var err = new Error('Book not found');
                err.status = 404;
                return next(err);
            }
            // Successful, so render.
            res.render('book_detail', { title: 'Title', book:  results.book, book_instances: results.book_instance } );
        });

    };

    // Display book create form on GET.
    exports.book_create_get = function(req, res, next) {

        // Get all authors and genres, which we can use for adding to our book.
        async.parallel({
            authors: function(callback) {
                Author.find(callback);
            },
            genres: function(callback) {
                Genre.find(callback);
            },
        }, function(err, results) {
            if (err) { return next(err); }
            res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres });
        });

    };

    // Handle book create on POST.
    exports.book_create_post = [
        // Convert the genre to an array.
        (req, res, next) => {
            if(!(req.body.genre instanceof Array)){
                if(typeof req.body.genre==='undefined')
                req.body.genre=[];
                else
                req.body.genre=new Array(req.body.genre);
            }
            next();
        },

        // Validate fields.
        body('title', 'Title must not be empty.').isLength({ min: 1 }).trim(),
        body('author', 'Author must not be empty.').isLength({ min: 1 }).trim(),
        body('summary', 'Summary must not be empty.').isLength({ min: 1 }).trim(),
        body('isbn', 'ISBN must not be empty').isLength({ min: 1 }).trim(),

        // Sanitize fields.
        sanitizeBody('*').escape(),
        sanitizeBody('genre.*').escape(),
        // Process request after validation and sanitization.
        (req, res, next) => {


            // Extract the validation errors from a request.
            const errors = validationResult(req);

            // Create a Book object with escaped and trimmed data.
            var book = new Book(
              { title: req.body.title,
                author: req.body.author,
                summary: req.body.summary,
                isbn: req.body.isbn,
                genre: req.body.genre
                });

            if (!errors.isEmpty()) {
                // There are errors. Render form again with sanitized values/error messages.

                // Get all authors and genres for form.
                async.parallel({
                    authors: function(callback) {
                        Author.find(callback);
                    },
                    genres: function(callback) {
                        Genre.find(callback);
                    },
                }, function(err, results) {
                    if (err) { return next(err); }

                    // Mark our selected genres as checked.
                    for (let i = 0; i < results.genres.length; i++) {
                        if (book.genre.indexOf(results.genres[i]._id) > -1) {
                            results.genres[i].checked='true';
                        }
                    }
                    res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres, book: book, errors: errors.array() });
                });
                return;
            }
            else {
                // Data from form is valid. Save book.
                book.save(function (err) {
                    if (err) { return next(err); }
                        // Successful - redirect to new book record.
                        res.redirect(book.url);
                    });
            }
        }
    ];



    // Display book delete form on GET.
    exports.book_delete_get = function(req, res, next) {

        async.parallel({
            book: function(callback) {
                Book.findById(req.params.id).populate('author').populate('genre').exec(callback);
            },
            book_bookinstances: function(callback) {
                BookInstance.find({ 'book': req.params.id }).exec(callback);
            },
        }, function(err, results) {
            if (err) { return next(err); }
            if (results.book==null) { // No results.
                res.redirect('/catalog/books');
            }
            // Successful, so render.
            res.render('book_delete', { title: 'Delete Book', book: results.book, book_instances: results.book_bookinstances } );
        });

    };

    // Handle book delete on POST.
    exports.book_delete_post = function(req, res, next) {

        // Assume the post has valid id (ie no validation/sanitization).

        async.parallel({
            book: function(callback) {
                Book.findById(req.body.id).populate('author').populate('genre').exec(callback);
            },
            book_bookinstances: function(callback) {
                BookInstance.find({ 'book': req.body.id }).exec(callback);
            },
        }, function(err, results) {
            if (err) { return next(err); }
            // Success
            if (results.book_bookinstances.length > 0) {
                // Book has book_instances. Render in same way as for GET route.
                res.render('book_delete', { title: 'Delete Book', book: results.book, book_instances: results.book_bookinstances } );
                return;
            }
            else {
                // Book has no BookInstance objects. Delete object and redirect to the list of books.
                Book.findByIdAndRemove(req.body.id, function deleteBook(err) {
                    if (err) { return next(err); }
                    // Success - got to books list.
                    res.redirect('/catalog/books');
                });

            }
        });

    };

    // Display book update form on GET.
    exports.book_update_get = function(req, res, next) {

        // Get book, authors and genres for form.
        async.parallel({
            book: function(callback) {
                Book.findById(req.params.id).populate('author').populate('genre').exec(callback);
            },
            authors: function(callback) {
                Author.find(callback);
            },
            genres: function(callback) {
                Genre.find(callback);
            },
            }, function(err, results) {
                if (err) { return next(err); }
                if (results.book==null) { // No results.
                    var err = new Error('Book not found');
                    err.status = 404;
                    return next(err);
                }
                // Success.
                // Mark our selected genres as checked.
                for (var all_g_iter = 0; all_g_iter < results.genres.length; all_g_iter++) {
                    for (var book_g_iter = 0; book_g_iter < results.book.genre.length; book_g_iter++) {
                        if (results.genres[all_g_iter]._id.toString()==results.book.genre[book_g_iter]._id.toString()) {
                            results.genres[all_g_iter].checked='true';
                        }
                    }
                }
                res.render('book_form', { title: 'Update Book', authors:results.authors, genres:results.genres, book: results.book });
            });

    };


    // Handle book update on POST.
    exports.book_update_post = [

        // Convert the genre to an array.
        (req, res, next) => {
            if(!(req.body.genre instanceof Array)){
                if(typeof req.body.genre==='undefined')
                req.body.genre=[];
                else
                req.body.genre=new Array(req.body.genre);
            }
            next();
        },

        // Validate fields.
        body('title', 'Title must not be empty.').isLength({ min: 1 }).trim(),
        body('author', 'Author must not be empty.').isLength({ min: 1 }).trim(),
        body('summary', 'Summary must not be empty.').isLength({ min: 1 }).trim(),
        body('isbn', 'ISBN must not be empty').isLength({ min: 1 }).trim(),

        // Sanitize fields.
        sanitizeBody('title').escape(),
        sanitizeBody('author').escape(),
        sanitizeBody('summary').escape(),
        sanitizeBody('isbn').escape(),
        sanitizeBody('genre.*').escape(),

        // Process request after validation and sanitization.
        (req, res, next) => {

            // Extract the validation errors from a request.
            const errors = validationResult(req);

            // Create a Book object with escaped/trimmed data and old id.
            var book = new Book(
              { title: req.body.title,
                author: req.body.author,
                summary: req.body.summary,
                isbn: req.body.isbn,
                genre: (typeof req.body.genre==='undefined') ? [] : req.body.genre,
                _id:req.params.id // This is required, or a new ID will be assigned!
                });

            if (!errors.isEmpty()) {
                // There are errors. Render form again with sanitized values/error messages.

                // Get all authors and genres for form
                async.parallel({
                    authors: function(callback) {
                        Author.find(callback);
                    },
                    genres: function(callback) {
                        Genre.find(callback);
                    },
                }, function(err, results) {
                    if (err) { return next(err); }

                    // Mark our selected genres as checked.
                    for (let i = 0; i < results.genres.length; i++) {
                        if (book.genre.indexOf(results.genres[i]._id) > -1) {
                            results.genres[i].checked='true';
                        }
                    }
                    res.render('book_form', { title: 'Update Book',authors:results.authors, genres:results.genres, book: book, errors: errors.array() });
                });
                return;
            }
            else {
                // Data from form is valid. Update the record.
                Book.findByIdAndUpdate(req.params.id, book, {}, function (err,thebook) {
                    if (err) { return next(err); }
                        // Successful - redirect to book detail page.
                        res.redirect(thebook.url);
                    });
            }
        }
    ];

    
   +---- bookinstanceController.js

    var BookInstance = require('../models/bookinstance')
    var Book = require('../models/book')
    var async = require('async')

    const { body,validationResult } = require('express-validator/check');
    const { sanitizeBody } = require('express-validator/filter');

    // Display list of all BookInstances.
    exports.bookinstance_list = function(req, res, next) {

      BookInstance.find()
        .populate('book')
        .exec(function (err, list_bookinstances) {
          if (err) { return next(err); }
          // Successful, so render.
          res.render('bookinstance_list', { title: 'Book Instance List', bookinstance_list:  list_bookinstances});
        })

    };

    // Display detail page for a specific BookInstance.
    exports.bookinstance_detail = function(req, res, next) {

        BookInstance.findById(req.params.id)
        .populate('book')
        .exec(function (err, bookinstance) {
          if (err) { return next(err); }
          if (bookinstance==null) { // No results.
              var err = new Error('Book copy not found');
              err.status = 404;
              return next(err);
            }
          // Successful, so render.
          res.render('bookinstance_detail', { title: 'Book:', bookinstance:  bookinstance});
        })

    };

    // Display BookInstance create form on GET.
    exports.bookinstance_create_get = function(req, res, next) {

          Book.find({},'title')
        .exec(function (err, books) {
          if (err) { return next(err); }
          // Successful, so render.
          res.render('bookinstance_form', {title: 'Create BookInstance', book_list:books } );
        });

    };

    // Handle BookInstance create on POST.
    exports.bookinstance_create_post = [

        // Validate fields.
        body('book', 'Book must be specified').isLength({ min: 1 }).trim(),
        body('imprint', 'Imprint must be specified').isLength({ min: 1 }).trim(),
        body('due_back', 'Invalid date').optional({ checkFalsy: true }).isISO8601(),

        // Sanitize fields.
        sanitizeBody('book').escape(),
        sanitizeBody('imprint').escape(),
        sanitizeBody('status').escape(),
        sanitizeBody('due_back').toDate(),

        // Process request after validation and sanitization.
        (req, res, next) => {

            // Extract the validation errors from a request.
            const errors = validationResult(req);

            // Create a BookInstance object with escaped and trimmed data.
            var bookinstance = new BookInstance(
              { book: req.body.book,
                imprint: req.body.imprint,
                status: req.body.status,
                due_back: req.body.due_back
                });

            if (!errors.isEmpty()) {
                // There are errors. Render form again with sanitized values and error messages.
                Book.find({},'title')
                    .exec(function (err, books) {
                        if (err) { return next(err); }
                        // Successful, so render.
                        res.render('bookinstance_form', { title: 'Create BookInstance', book_list : books, selected_book : bookinstance.book._id , errors: errors.array(), bookinstance:bookinstance });
                });
                return;
            }
            else {
                // Data from form is valid
                bookinstance.save(function (err) {
                    if (err) { return next(err); }
                        // Successful - redirect to new record.
                        res.redirect(bookinstance.url);
                    });
            }
        }
    ];



    // Display BookInstance delete form on GET.
    exports.bookinstance_delete_get = function(req, res, next) {

      BookInstance.findById(req.params.id)
      .populate('book')
      .exec(function (err, bookinstance) {
        if (err) { return next(err); }
        if (bookinstance==null) { // No results.
            res.redirect('/catalog/bookinstances');
        }
        // Successful, so render.
        res.render('bookinstance_delete', { title: 'Delete BookInstance', bookinstance:  bookinstance});
      })

    };

    // Handle BookInstance delete on POST.
    exports.bookinstance_delete_post = function(req, res, next) {

      // Assume valid BookInstance id in field.
      BookInstance.findByIdAndRemove(req.body.id, function deleteBookInstance(err) {
        if (err) { return next(err); }
        // Success, so redirect to list of BookInstance items.
        res.redirect('/catalog/bookinstances');
        });

    };

    // Display BookInstance update form on GET.
    exports.bookinstance_update_get = function(req, res, next) {

      // Get book, authors and genres for form.
      async.parallel({
        bookinstance: function(callback) {
          BookInstance.findById(req.params.id).populate('book').exec(callback)
        },
        books: function(callback) {
          Book.find(callback)
        },

        }, function(err, results) {
          if (err) { return next(err); }
          if (results.bookinstance==null) { // No results.
            var err = new Error('Book copy not found');
            err.status = 404;
            return next(err);
          }
          // Success.
          res.render('bookinstance_form', { title: 'Update  BookInstance', book_list : results.books, selected_book : results.bookinstance.book._id, bookinstance:results.bookinstance });
        });

    };

    // Handle BookInstance update on POST.
    exports.bookinstance_update_post = [

        // Validate fields.
        body('book', 'Book must be specified').isLength({ min: 1 }).trim(),
        body('imprint', 'Imprint must be specified').isLength({ min: 1 }).trim(),
        body('due_back', 'Invalid date').optional({ checkFalsy: true }).isISO8601(),

        // Sanitize fields.
        sanitizeBody('book').escape(),
        sanitizeBody('imprint').escape(),
        sanitizeBody('status').escape(),
        sanitizeBody('due_back').toDate(),

        // Process request after validation and sanitization.
        (req, res, next) => {

          // Extract the validation errors from a request.
          const errors = validationResult(req);

          // Create a BookInstance object with escaped/trimmed data and current id.
          var bookinstance = new BookInstance(
            {
              book: req.body.book,
              imprint: req.body.imprint,
              status: req.body.status,
              due_back: req.body.due_back,
              _id: req.params.id
            }
          );

          if (!errors.isEmpty()) {
            // There are errors so render the form again, passing sanitized values and errors.
            Book.find({},'title')
              .exec(function (err, books) {
                if (err) { return next(err); }
                // Successful, so render.
                res.render('bookinstance_form', { title: 'Update BookInstance', book_list : books, selected_book : bookinstance.book._id , errors: errors.array(), bookinstance:bookinstance });
            });
            return;
          }
          else {
            // Data from form is valid.
            BookInstance.findByIdAndUpdate(req.params.id, bookinstance, {}, function (err,thebookinstance) {
              if (err) { return next(err); }
                // Successful - redirect to detail page.
                res.redirect(thebookinstance.url);
              });
          }
        }
    ];
    
   +---- genreController.js

    var Genre = require('../models/genre');
    var Book = require('../models/book');
    var async = require('async');

    const { body,validationResult } = require('express-validator/check');
    const { sanitizeBody } = require('express-validator/filter');

    // Display list of all Genre.
    exports.genre_list = function(req, res, next) {

      Genre.find()
        .sort([['name', 'ascending']])
        .exec(function (err, list_genres) {
          if (err) { return next(err); }
          // Successful, so render.
          res.render('genre_list', { title: 'Genre List', list_genres:  list_genres});
        });

    };

    // Display detail page for a specific Genre.
    exports.genre_detail = function(req, res, next) {

      async.parallel({
        genre: function(callback) {
          Genre.findById(req.params.id)
            .exec(callback);
        },

        genre_books: function(callback) {
          Book.find({ 'genre': req.params.id })
          .exec(callback);
        },

      }, function(err, results) {
        if (err) { return next(err); }
        if (results.genre==null) { // No results.
          var err = new Error('Genre not found');
          err.status = 404;
          return next(err);
        }
        // Successful, so render.
        res.render('genre_detail', { title: 'Genre Detail', genre: results.genre, genre_books: results.genre_books } );
      });

    };

    // Display Genre create form on GET.
    exports.genre_create_get = function(req, res, next) {
        res.render('genre_form', { title: 'Create Genre'});
    };

    // Handle Genre create on POST.
    exports.genre_create_post = [

        // Validate that the name field is not empty.
        body('name', 'Genre name required').isLength({ min: 1 }).trim(),

        // Sanitize (trim) the name field.
        sanitizeBody('name').escape(),

        // Process request after validation and sanitization.
        (req, res, next) => {

            // Extract the validation errors from a request.
            const errors = validationResult(req);

            // Create a genre object with escaped and trimmed data.
            var genre = new Genre(
              { name: req.body.name }
            );

            if (!errors.isEmpty()) {
              // There are errors. Render the form again with sanitized values/error messages.
              res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors.array()});
            return;
            }
            else {
              // Data from form is valid.
              // Check if Genre with same name already exists.
              Genre.findOne({ 'name': req.body.name })
                .exec( function(err, found_genre) {
                    if (err) { return next(err); }

                    if (found_genre) {
                      // Genre exists, redirect to its detail page.
                      res.redirect(found_genre.url);
                    }
                    else {

                      genre.save(function (err) {
                        if (err) { return next(err); }
                        // Genre saved. Redirect to genre detail page.
                        res.redirect(genre.url);
                      });

                    }

                });
            }
        }
    ];

    // Display Genre delete form on GET.
    exports.genre_delete_get = function(req, res, next) {

      async.parallel({
        genre: function(callback) {
            Genre.findById(req.params.id).exec(callback);
        },
        genre_books: function(callback) {
            Book.find({ 'genre': req.params.id }).exec(callback);
        },
      }, function(err, results) {
        if (err) { return next(err); }
        if (results.genre==null) { // No results.
            res.redirect('/catalog/genres');
        }
        // Successful, so render.
        res.render('genre_delete', { title: 'Delete Genre', genre: results.genre, genre_books: results.genre_books } );
      });

    };

    // Handle Genre delete on POST.
    exports.genre_delete_post = function(req, res, next) {

      async.parallel({
        genre: function(callback) {
          Genre.findById(req.params.id).exec(callback);
        },
        genre_books: function(callback) {
          Book.find({ 'genre': req.params.id }).exec(callback);
        },
      }, function(err, results) {
        if (err) { return next(err); }
        // Success
        if (results.genre_books.length > 0) {
          // Genre has books. Render in same way as for GET route.
          res.render('genre_delete', { title: 'Delete Genre', genre: results.genre, genre_books: results.genre_books } );
          return;
        }
        else {

          // Genre has no books. Delete object and redirect to the list of genres.
          Genre.findByIdAndRemove(req.body.id, function deleteGenre(err) {
            if (err) { return next(err); }
            // Success - go to genres list.
            res.redirect('/catalog/genres');
          });

        }
      });

    };

    // Display Genre update form on GET.
    exports.genre_update_get = function(req, res, next) {

      Genre.findById(req.params.id, function(err, genre) {
        if (err) { return next(err); }
        if (genre==null) { // No results.
          var err = new Error('Genre not found');
          err.status = 404;
          return next(err);
        }
        // Success.
        res.render('genre_form', { title: 'Update Genre', genre: genre });
      });

    };

    // Handle Genre update on POST.
    exports.genre_update_post = [

      // Validate that the name field is not empty.
      body('name', 'Genre name required').isLength({ min: 1 }).trim(),

      // Sanitize (escape) the name field.
      sanitizeBody('name').escape(),

      // Process request after validation and sanitization.
      (req, res, next) => {

        // Extract the validation errors from a request .
        const errors = validationResult(req);

        // Create a genre object with escaped and trimmed data (and the old id!)
        var genre = new Genre(
          {
          name: req.body.name,
          _id: req.params.id
          }
        );

        if (!errors.isEmpty()) {
          // There are errors. Render the form again with sanitized values and error messages.
          res.render('genre_form', { title: 'Update Genre', genre: genre, errors: errors.array()});
          return;
        }
        else {
          // Data from form is valid. Update the record.
          Genre.findByIdAndUpdate(req.params.id, genre, {}, function (err,thegenre) {
            if (err) { return next(err); }
                // Successful - redirect to genre detail page.
                res.redirect(thegenre.url);
            });
        }
      }
    ];
    
   models
   +---- author.js

    var mongoose = require('mongoose');
    var moment = require('moment'); // For date handling.

    var Schema = mongoose.Schema;

    var AuthorSchema = new Schema(
        {
        first_name: {type: String, required: true, max: 100},
        family_name: {type: String, required: true, max: 100},
        date_of_birth: { type: Date },
        date_of_death: { type: Date },
        }
      );

    // Virtual for author "full" name.
    AuthorSchema
    .virtual('name')
    .get(function () {
      return this.family_name +', '+this.first_name;
    });

    // Virtual for this author instance URL.
    AuthorSchema
    .virtual('url')
    .get(function () {
      return '/catalog/author/'+this._id
    });

    AuthorSchema
    .virtual('lifespan')
    .get(function () {
      var lifetime_string='';
      if (this.date_of_birth) {
          lifetime_string=moment(this.date_of_birth).format('MMMM Do, YYYY');
          }
      lifetime_string+=' - ';
      if (this.date_of_death) {
          lifetime_string+=moment(this.date_of_death).format('MMMM Do, YYYY');
          }
      return lifetime_string
    });

    AuthorSchema
    .virtual('date_of_birth_yyyy_mm_dd')
    .get(function () {
      return moment(this.date_of_birth).format('YYYY-MM-DD');
    });

    AuthorSchema
    .virtual('date_of_death_yyyy_mm_dd')
    .get(function () {
      return moment(this.date_of_death).format('YYYY-MM-DD');
    });

    // Export model.
    module.exports = mongoose.model('Author', AuthorSchema);
    
   +---- book.js

    var mongoose = require('mongoose');

    var Schema = mongoose.Schema;

    var BookSchema = new Schema({
        title: {type: String, required: true},
        author: { type: Schema.ObjectId, ref: 'Author', required: true },
        summary: {type: String, required: true},
        isbn: {type: String, required: true},
        genre: [{ type: Schema.ObjectId, ref: 'Genre' }]
    });

    // Virtual for this book instance URL.
    BookSchema
    .virtual('url')
    .get(function () {
      return '/catalog/book/'+this._id;
    });

    // Export model.
    module.exports = mongoose.model('Book', BookSchema);
    
   +---- bookinstance.js

    var mongoose = require('mongoose');
    var moment = require('moment');

    var Schema = mongoose.Schema;

    var BookInstanceSchema = new Schema({
        book: { type: Schema.ObjectId, ref: 'Book', required: true }, // Reference to the associated book.
        imprint: {type: String, required: true},
        status: {type: String, required: true, enum:['Available', 'Maintenance', 'Loaned', 'Reserved'], default:'Maintenance'},
        due_back: { type: Date, default: Date.now },
    });

    // Virtual for this bookinstance object's URL.
    BookInstanceSchema
    .virtual('url')
    .get(function () {
      return '/catalog/bookinstance/'+this._id;
    });


    BookInstanceSchema
    .virtual('due_back_formatted')
    .get(function () {
      return moment(this.due_back).format('MMMM Do, YYYY');
    });

    BookInstanceSchema
    .virtual('due_back_yyyy_mm_dd')
    .get(function () {
      return moment(this.due_back).format('YYYY-MM-DD');
    });


    // Export model.
    module.exports = mongoose.model('BookInstance', BookInstanceSchema);
    
   +---- genre.js

    var mongoose = require('mongoose');

    var Schema = mongoose.Schema;

    var GenreSchema = new Schema({
        name: {type: String, required: true, min: 3, max: 100}
    });

    // Virtual for this genre instance URL.
    GenreSchema
    .virtual('url')
    .get(function () {
      return '/catalog/genre/'+this._id;
    });

    // Export model.
    module.exports = mongoose.model('Genre', GenreSchema);
    
   package.json

    {
      "name": "express-locallibrary-tutorial",
      "version": "0.0.0",
      "engines": {
        "node": "10.15.1"
      },
      "private": true,
      "scripts": {
        "start": "node ./bin/www",
        "devstart": "nodemon ./bin/www"
      },
      "dependencies": {
        "async": "^2.6.2",
        "compression": "^1.7.3",
        "cookie-parser": "~1.4.3",
        "debug": "~2.6.9",
        "express": "~4.16.0",
        "express-validator": "^5.3.1",
        "helmet": "^3.15.1",
        "http-errors": "~1.6.2",
        "moment": "^2.24.0",
        "mongoose": "^5.4.13",
        "morgan": "~1.9.0",
        "pug": "2.0.0-beta11"
      },
      "devDependencies": {
        "nodemon": "^1.18.10"
      }
    }
    
   populatedb.js

    #! /usr/bin/env node

    console.log('This script populates some test books, authors, genres and bookinstances to your database. Specified database as argument - e.g.: populatedb mongodb+srv://cooluser:coolpassword@cluster0-mbdj7.mongodb.net/local_library?retryWrites=true');

    // Get arguments passed on command line
    var userArgs = process.argv.slice(2);
    /*
    if (!userArgs[0].startsWith('mongodb')) {
        console.log('ERROR: You need to specify a valid mongodb URL as the first argument');
        return
    }
    */
    var async = require('async')
    var Book = require('./models/book')
    var Author = require('./models/author')
    var Genre = require('./models/genre')
    var BookInstance = require('./models/bookinstance')


    var mongoose = require('mongoose');
    var mongoDB = userArgs[0];
    mongoose.connect(mongoDB, { useNewUrlParser: true });
    mongoose.Promise = global.Promise;
    var db = mongoose.connection;
    db.on('error', console.error.bind(console, 'MongoDB connection error:'));

    var authors = []
    var genres = []
    var books = []
    var bookinstances = []

    function authorCreate(first_name, family_name, d_birth, d_death, cb) {
      authordetail = {first_name:first_name , family_name: family_name }
      if (d_birth != false) authordetail.date_of_birth = d_birth
      if (d_death != false) authordetail.date_of_death = d_death

      var author = new Author(authordetail);

      author.save(function (err) {
        if (err) {
          cb(err, null)
          return
        }
        console.log('New Author: ' + author);
        authors.push(author)
        cb(null, author)
      }  );
    }

    function genreCreate(name, cb) {
      var genre = new Genre({ name: name });

      genre.save(function (err) {
        if (err) {
          cb(err, null);
          return;
        }
        console.log('New Genre: ' + genre);
        genres.push(genre)
        cb(null, genre);
      }   );
    }

    function bookCreate(title, summary, isbn, author, genre, cb) {
      bookdetail = {
        title: title,
        summary: summary,
        author: author,
        isbn: isbn
      }
      if (genre != false) bookdetail.genre = genre

      var book = new Book(bookdetail);
      book.save(function (err) {
        if (err) {
          cb(err, null)
          return
        }
        console.log('New Book: ' + book);
        books.push(book)
        cb(null, book)
      }  );
    }


    function bookInstanceCreate(book, imprint, due_back, status, cb) {
      bookinstancedetail = {
        book: book,
        imprint: imprint
      }
      if (due_back != false) bookinstancedetail.due_back = due_back
      if (status != false) bookinstancedetail.status = status

      var bookinstance = new BookInstance(bookinstancedetail);
      bookinstance.save(function (err) {
        if (err) {
          console.log('ERROR CREATING BookInstance: ' + bookinstance);
          cb(err, null)
          return
        }
        console.log('New BookInstance: ' + bookinstance);
        bookinstances.push(bookinstance)
        cb(null, book)
      }  );
    }


    function createGenreAuthors(cb) {
      async.series([
        function(callback) {
          authorCreate('Patrick', 'Rothfuss', '1973-06-06', false, callback);
        },
        function(callback) {
          authorCreate('Ben', 'Bova', '1932-11-8', false, callback);
        },
        function(callback) {
          authorCreate('Isaac', 'Asimov', '1920-01-02', '1992-04-06', callback);
        },
        function(callback) {
          authorCreate('Bob', 'Billings', false, false, callback);
        },
        function(callback) {
          authorCreate('Jim', 'Jones', '1971-12-16', false, callback);
        },
        function(callback) {
          genreCreate("Fantasy", callback);
        },
        function(callback) {
          genreCreate("Science Fiction", callback);
        },
        function(callback) {
          genreCreate("French Poetry", callback);
        },
        ],
        // optional callback
        cb
      );
    }


    function createBooks(cb) {
      async.parallel([
        function(callback) {
          bookCreate(
            'The Name of the Wind (The Kingkiller Chronicle, #1)',
            'I have stolen princesses back from sleeping barrow kings. I burned down the town of Trebon. I have spent the night with Felurian and left with both my sanity and my life. I was expelled from the University at a younger age than most people are allowed in. I tread paths by moonlight that others fear to speak of during day. I have talked to Gods, loved women, and written songs that make the minstrels weep.',
            '9781473211896',
            authors[0],
            [genres[0],],
            callback
          );
        },
        function(callback) {
          bookCreate(
            "The Wise Man's Fear (The Kingkiller Chronicle, #2)",
            'Picking up the tale of Kvothe Kingkiller once again, we follow him into exile, into political intrigue, courtship, adventure, love and magic... and further along the path that has turned Kvothe, the mightiest magician of his age, a legend in his own time, into Kote, the unassuming pub landlord.',
            '9788401352836',
            authors[0],
            [genres[0],],
            callback
          );
        },
        function(callback) {
          bookCreate(
            "The Slow Regard of Silent Things (Kingkiller Chronicle)",
            'Deep below the University, there is a dark place. Few people know of it: a broken web of ancient passageways and abandoned rooms. A young woman lives there, tucked among the sprawling tunnels of the Underthing, snug in the heart of this forgotten place.',
            '9780756411336',
            authors[0],
            [genres[0],],
            callback
          );
        },
        function(callback) {
          bookCreate(
            "Apes and Angels", "Humankind headed out to the stars not for conquest, nor exploration, nor even for curiosity. Humans went to the stars in a desperate crusade to save intelligent life wherever they found it. A wave of death is spreading through the Milky Way galaxy, an expanding sphere of lethal gamma ...",
            '9780765379528',
            authors[1],
            [genres[1],],
            callback
          );
        },
        function(callback) {
          bookCreate(
            "Death Wave","In Ben Bova's previous novel New Earth, Jordan Kell led the first human mission beyond the solar system. They discovered the ruins of an ancient alien civilization. But one alien AI survived, and it revealed to Jordan Kell that an explosion in the black hole at the heart of the Milky Way galaxy has created a wave of deadly radiation, expanding out from the core toward Earth. Unless the human race acts to save itself, all life on Earth will be wiped out...",
            '9780765379504',
            authors[1],
            [genres[1],],
            callback
          );
        },
        function(callback) {
          bookCreate(
            'Test Book 1',
            'Summary of test book 1',
            'ISBN111111',
            authors[4],
            [genres[0],
            genres[1]],
            callback
          );
        },
        function(callback) {
          bookCreate(
            'Test Book 2',
            'Summary of test book 2',
            'ISBN222222',
            authors[4],
            false,
            callback
          )
        }
        ],
        // optional callback
        cb
      );
    }


    function createBookInstances(cb) {
      async.parallel([
        function(callback) {
          bookInstanceCreate(books[0], 'London Gollancz, 2014.', false, 'Available', callback)
        },
        function(callback) {
          bookInstanceCreate(books[1], ' Gollancz, 2011.', false, 'Loaned', callback)
        },
        function(callback) {
          bookInstanceCreate(books[2], ' Gollancz, 2015.', false, false, callback)
        },
        function(callback) {
          bookInstanceCreate(books[3], 'New York Tom Doherty Associates, 2016.', false, 'Available', callback)
        },
        function(callback) {
          bookInstanceCreate(books[3], 'New York Tom Doherty Associates, 2016.', false, 'Available', callback)
        },
        function(callback) {
          bookInstanceCreate(books[3], 'New York Tom Doherty Associates, 2016.', false, 'Available', callback)
        },
        function(callback) {
          bookInstanceCreate(books[4], 'New York, NY Tom Doherty Associates, LLC, 2015.', false, 'Available', callback)
        },
        function(callback) {
          bookInstanceCreate(books[4], 'New York, NY Tom Doherty Associates, LLC, 2015.', false, 'Maintenance', callback)
        },
        function(callback) {
          bookInstanceCreate(books[4], 'New York, NY Tom Doherty Associates, LLC, 2015.', false, 'Loaned', callback)
        },
        function(callback) {
          bookInstanceCreate(books[0], 'Imprint XXX2', false, false, callback)
        },
        function(callback) {
          bookInstanceCreate(books[1], 'Imprint XXX3', false, false, callback)
        }
        ],
        // Optional callback
        cb
      );
    }

    async.series([
      createGenreAuthors,
      createBooks,
      createBookInstances
    ],
    // Optional callback
    function(err, results) {
      if (err) {
        console.log('FINAL ERR: '+err);
      }
      else {
        console.log('BOOKInstances: '+bookinstances);
      }
      // All done, disconnect from database
      mongoose.connection.close();
    });
    
   public
   +---- images
   +---- stylesheets
   |      +---- style.css

    .sidebar-nav {
        margin-top: 20px;
        padding: 0;
        list-style: none;
    }
   routes
   +---- catalog.js

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


    // Require our controllers.
    var book_controller = require('../controllers/bookController');
    var author_controller = require('../controllers/authorController');
    var genre_controller = require('../controllers/genreController');
    var book_instance_controller = require('../controllers/bookinstanceController');


    /// BOOK ROUTES ///

    // GET catalog home page.
    router.get('/', book_controller.index);

    // GET request for creating a Book. NOTE This must come before routes that display Book (uses id).
    router.get('/book/create', book_controller.book_create_get);

    // POST request for creating Book.
    router.post('/book/create', book_controller.book_create_post);

    // GET request to delete Book.
    router.get('/book/:id/delete', book_controller.book_delete_get);

    // POST request to delete Book.
    router.post('/book/:id/delete', book_controller.book_delete_post);

    // GET request to update Book.
    router.get('/book/:id/update', book_controller.book_update_get);

    // POST request to update Book.
    router.post('/book/:id/update', book_controller.book_update_post);

    // GET request for one Book.
    router.get('/book/:id', book_controller.book_detail);

    // GET request for list of all Book.
    router.get('/books', book_controller.book_list);

    /// AUTHOR ROUTES ///

    // GET request for creating Author. NOTE This must come before route for id (i.e. display author).
    router.get('/author/create', author_controller.author_create_get);

    // POST request for creating Author.
    router.post('/author/create', author_controller.author_create_post);

    // GET request to delete Author.
    router.get('/author/:id/delete', author_controller.author_delete_get);

    // POST request to delete Author
    router.post('/author/:id/delete', author_controller.author_delete_post);

    // GET request to update Author.
    router.get('/author/:id/update', author_controller.author_update_get);

    // POST request to update Author.
    router.post('/author/:id/update', author_controller.author_update_post);

    // GET request for one Author.
    router.get('/author/:id', author_controller.author_detail);

    // GET request for list of all Authors.
    router.get('/authors', author_controller.author_list);


    /// GENRE ROUTES ///

    // GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id).
    router.get('/genre/create', genre_controller.genre_create_get);

    // POST request for creating Genre.
    router.post('/genre/create', genre_controller.genre_create_post);

    // GET request to delete Genre.
    router.get('/genre/:id/delete', genre_controller.genre_delete_get);

    // POST request to delete Genre.
    router.post('/genre/:id/delete', genre_controller.genre_delete_post);

    // GET request to update Genre.
    router.get('/genre/:id/update', genre_controller.genre_update_get);

    // POST request to update Genre.
    router.post('/genre/:id/update', genre_controller.genre_update_post);

    // GET request for one Genre.
    router.get('/genre/:id', genre_controller.genre_detail);

    // GET request for list of all Genre.
    router.get('/genres', genre_controller.genre_list);


    /// BOOKINSTANCE ROUTES ///

    // GET request for creating a BookInstance. NOTE This must come before route that displays BookInstance (uses id).
    router.get('/bookinstance/create', book_instance_controller.bookinstance_create_get);

    // POST request for creating BookInstance.
    router.post('/bookinstance/create', book_instance_controller.bookinstance_create_post);

    // GET request to delete BookInstance.
    router.get('/bookinstance/:id/delete', book_instance_controller.bookinstance_delete_get);

    // POST request to delete BookInstance.
    router.post('/bookinstance/:id/delete', book_instance_controller.bookinstance_delete_post);

    // GET request to update BookInstance.
    router.get('/bookinstance/:id/update', book_instance_controller.bookinstance_update_get);

    // POST request to update BookInstance.
    router.post('/bookinstance/:id/update', book_instance_controller.bookinstance_update_post);

    // GET request for one BookInstance.
    router.get('/bookinstance/:id', book_instance_controller.bookinstance_detail);

    // GET request for list of all BookInstance.
    router.get('/bookinstances', book_instance_controller.bookinstance_list);


    module.exports = router;
    
   +---- index.js

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

    // GET home page.
    router.get('/', function(req, res) {
      res.redirect('/catalog');
    });

    module.exports = router;
    
   +---- users.js

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

    // GET users listing.
    router.get('/', function(req, res, next) {
      res.send('respond with a resource');
    });

    module.exports = router;
    
   views
   +---- author_delete.pug

    extends layout

    block content

      h1 #{title}: #{author.name}
      p= author.lifespan

      if author_books.length

        p #[strong Delete the following books before attempting to delete this author.]

        div(style='margin-left:20px;margin-top:20px')

          h4 Books

          dl
          each book in author_books
            dt
              a(href=book.url) #{book.title}
            dd #{book.summary}

      else
        p Do you really want to delete this Author?

        form(method='POST' action='')
          div.form-group
            input#authorid.form-control(type='hidden',name='authorid', required='true', value=author._id )

          button.btn.btn-primary(type='submit') Delete
   +---- author_detail.pug

    extends layout

    block content

      h1 Author: #{author.name}
      p #{author.lifespan}

      div(style='margin-left:20px;margin-top:20px')

        h4 Books

        dl
        each book in author_books
          dt
            a(href=book.url) #{book.title}
          dd #{book.summary}

        else
          p This author has no books.

      hr
      p
        a(href=author.url+'/delete') Delete author
      p
        a(href=author.url+'/update') Update author
    
   +---- author_form.pug

    extends layout

    block content
      h1=title

      form(method='POST' action='')
        div.form-group
          label(for='first_name') First Name:
          input#first_name.form-control(type='text', placeholder='First name (Christian)' name='first_name' required='true' value=(undefined===author ? '' : author.first_name) )
          label(for='family_name') Family Name:
          input#family_name.form-control(type='text', placeholder='Family name (Surname)' name='family_name' required='true' value=(undefined===author ? '' : author.family_name))
        div.form-group
          label(for='date_of_birth') Date of birth:
          input#date_of_birth.form-control(type='date', name='date_of_birth' value=(undefined===author ? '' : author.date_of_birth_yyyy_mm_dd) )
        div.form-group
          label(for='date_of_death') Date of death:
          input#date_of_death.form-control(type='date', name='date_of_death' value=(undefined===author ? '' : author.date_of_death_yyyy_mm_dd) )
        button.btn.btn-primary(type='submit') Submit

      if errors
        ul
          for error in errors
            li!= error.msg
   +---- author_list.pug

    extends layout

    block content
      h1= title

      ul
      each author in author_list
        li
          a(href=author.url) #{author.name}
          | (#{author.lifespan})

      else
        li There are no authors.
    
   +---- book_delete.pug

    extends layout

    block content

      h1 #{title}: #{book.title}

      p #[strong Author:]
        a(href=book.author.url) #{book.author.name}
      p #[strong Summary:] #{book.summary}
      p #[strong ISBN:] #{book.isbn}
      p #[strong Genre:]
        each val in book.genre
          a(href=val.url) #{val.name}
          |,

      hr

      if book_instances.length

        p #[strong Delete the following copies before attempting to delete this Book.]

        div(style='margin-left:20px;margin-top:20px')

          h4 Copies

            each book_copy in book_instances
              hr
              if book_copy.status=='Available'
                p.text-success #{book_copy.status}
              else if book_copy.status=='Maintenance'
                p.text-danger #{book_copy.status}
              else
                p.text-warning #{book_copy.status}
              p #[strong Imprint:] #{book_copy.imprint}
              if book_copy.status!='Available'
                p #[strong Due back:] #{book_copy.due_back}
              p #[strong Id:]
                a(href=book_copy.url) #{book_copy._id}

      else
        p Do you really want to delete this Book?

        form(method='POST' action='')
          div.form-group
            input#id.form-control(type='hidden',name='id', required='true', value=book._id )

          button.btn.btn-primary(type='submit') Delete
   +---- book_detail.pug

    extends layout

    block content
      h1 #{title}: #{book.title}

      p #[strong Author:]
        a(href=book.author.url) #{book.author.name}
      p #[strong Summary:] #{book.summary}
      p #[strong ISBN:] #{book.isbn}
      p #[strong Genre:]
        each val in book.genre
          a(href=val.url) #{val.name}
          |,

      div(style='margin-left:20px;margin-top:20px')
        h4 Copies

        each val in book_instances
          hr
          if val.status=='Available'
            p.text-success #{val.status}
          else if val.status=='Maintenance'
            p.text-danger #{val.status}
          else
            p.text-warning #{val.status}
          p #[strong Imprint:] #{val.imprint}
          if val.status!='Available'
            p #[strong Due back:] #{val.due_back}
          p #[strong Id:]
            a(href=val.url) #{val._id}

        else
          p There are no copies of this book in the library.

      hr
      p
        a(href=book.url+'/delete') Delete Book
      p
        a(href=book.url+'/update') Update Book
   +---- book_form.pug

    extends layout

    block content
      h1= title

      form(method='POST' action='')
        div.form-group
          label(for='title') Title:
          input#title.form-control(type='text', placeholder='Name of book' name='title' required='true' value=(undefined===book ? '' : book.title) )
        div.form-group
          label(for='author') Author:
          select#author.form-control(type='select', placeholder='Select author' name='author' required='true' )
            - authors.sort(function(a, b) {let textA = a.family_name.toUpperCase(); let textB = b.family_name.toUpperCase(); return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;});
            for author in authors
              if book
                option(value=author._id selected=(author._id.toString()==book.author._id.toString() ? 'selected' : false) ) #{author.name}
              else
                option(value=author._id) #{author.name}
        div.form-group
          label(for='summary') Summary:
          input#summary.form-control(type='textarea', placeholder='Summary' name='summary' value=(undefined===book ? '' : book.summary) required='true')
        div.form-group
          label(for='isbn') ISBN:
          input#isbn.form-control(type='text', placeholder='ISBN13' name='isbn' value=(undefined===book ? '' : book.isbn) required='true')
        div.form-group
          label Genre:
          div
            for genre in genres
              div(style='display: inline; padding-right:10px;')
                input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id, checked=genre.checked )
                label(for=genre._id) #{genre.name}
        button.btn.btn-primary(type='submit') Submit

      if errors
        ul
          for error in errors
            li!= error.msg
    
   +---- book_list.pug

    extends layout

    block content
      h1= title

      ul
      - book_list.sort(function(a, b) {let textA = a.title.toUpperCase(); let textB = b.title.toUpperCase(); return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;});
      each book in book_list
        li
          a(href=book.url) #{book.title}
          | (#{book.author.name})

      else
        li There are no books.
    
   +---- bookinstance_delete.pug

    extends layout

    block content

      h1= title

      p #[strong Do you really want to delete this BookInstance?]

      div(style="margin-left:20px")

        p #[strong ID]: #{bookinstance._id}

        p #[strong Title:]
          a(href=bookinstance.book.url) #{bookinstance.book.title}

        p #[strong Imprint:] #{bookinstance.imprint}

        p #[strong Status:]
          if bookinstance.status=='Available'
            span.text-success #{bookinstance.status}
          else if bookinstance.status=='Maintenance'
            span.text-danger #{bookinstance.status}
          else
            span.text-warning #{bookinstance.status}

        if bookinstance.status!='Available'
          p #[strong Due back:] #{bookinstance.due_back_formatted}

      form(method='POST' action='')
        div.form-group
          input#id.form-control(type='hidden',name='id', required='true', value=bookinstance._id )

        button.btn.btn-primary(type='submit') Delete
   +---- bookinstance_detail.pug

    extends layout

    block content

      h1 ID: #{bookinstance._id}

      p #[strong Title:]
        a(href=bookinstance.book.url) #{bookinstance.book.title}
      p #[strong Imprint:] #{bookinstance.imprint}

      p #[strong Status:]
        if bookinstance.status=='Available'
          span.text-success #{bookinstance.status}
        else if bookinstance.status=='Maintenance'
          span.text-danger #{bookinstance.status}
        else
          span.text-warning #{bookinstance.status}

      if bookinstance.status!='Available'
        p #[strong Due back:] #{bookinstance.due_back_formatted}

      hr
      p
        a(href=bookinstance.url+'/delete') Delete BookInstance
      p
        a(href=bookinstance.url+'/update') Update BookInstance
   +---- bookinstance_form.pug

    extends layout

    block content
      h1=title

      form(method='POST' action='')
        div.form-group
          label(for='book') Book:
          select#book.form-control(type='select', placeholder='Select book' name='book' required='true' )
            - book_list.sort(function(a, b) {let textA = a.title.toUpperCase(); let textB = b.title.toUpperCase(); return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;});
            for book in book_list
              option(value=book._id, selected=(selected_book==book._id.toString() ? 'selected' : false) ) #{book.title}

        div.form-group
          label(for='imprint') Imprint:
          input#imprint.form-control(type='text', placeholder='Publisher and date information' name='imprint' required='true' value=(undefined===bookinstance ? '' : bookinstance.imprint) )
        div.form-group
          label(for='due_back') Date when book available:
          input#due_back.form-control(type='date', name='due_back' value=(undefined===bookinstance ? '' : bookinstance.due_back_yyyy_mm_dd))

        div.form-group
          label(for='status') Status:
          select#status.form-control(type='select', placeholder='Select status' name='status' required='true' )
            option(value='Maintenance' selected=(undefined===bookinstance || bookinstance.status!='Maintenance' ? false:'selected')) Maintenance
            option(value='Available' selected=(undefined===bookinstance || bookinstance.status!='Available' ? false:'selected')) Available
            option(value='Loaned' selected=(undefined===bookinstance || bookinstance.status!='Loaned' ? false:'selected')) Loaned
            option(value='Reserved' selected=(undefined===bookinstance || bookinstance.status!='Reserved' ? false:'selected')) Reserved

        button.btn.btn-primary(type='submit') Submit

      if errors
        ul
          for error in errors
            li!= error.msg
    
   +---- bookinstance_list.pug

    extends layout

    block content
      h1= title

      ul
      each val in bookinstance_list
        li
          a(href=val.url) #{val.book.title} : #{val.imprint} -
          if val.status=='Available'
            span.text-success #{val.status}
          else if val.status=='Maintenance'
            span.text-danger #{val.status}
          else
            span.text-warning #{val.status}
          if val.status!='Available'
            span  (Due: #{val.due_back_formatted} )


      else
        li There are no book copies in this library
    
   +---- error.pug

    extends layout

    block content
      h1= message
      h2= error.status
      pre #{error.stack}
    
   +---- genre_delete.pug

    extends layout

    block content

      h1 Delete Genre: #{genre.name}

      if genre_books.length

        p #[strong Delete the following books before attempting to delete this genre.]

        div(style='margin-left:20px;margin-top:20px')

          h4 Books

          dl
          each book in genre_books
            dt
              a(href=book.url) #{book.title}
            dd #{book.summary}

      else
        p Do you really want to delete this Genre?

        form(method='POST' action='')
          div.form-group
            input#id.form-control(type='hidden',name='id', required='true', value=genre._id )

          button.btn.btn-primary(type='submit') Delete
   +---- genre_detail.pug

    extends layout

    block content

      h1 Genre: #{genre.name}

      div(style='margin-left:20px;margin-top:20px')

        h4 Books

        dl
        each book in genre_books
          dt
            a(href=book.url) #{book.title}
          dd #{book.summary}

        else
          p This genre has no books.

      hr
      p
        a(href=genre.url+'/delete') Delete genre
      p
        a(href=genre.url+'/update') Update genre
   +---- genre_form.pug

    extends layout

    block content

      h1 #{title}

      form(method='POST' action='')
        div.form-group
          label(for='name') Genre:
          input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' required='true' value=(undefined===genre ? '' : genre.name) )
        button.btn.btn-primary(type='submit') Submit

      if errors
        ul
          for error in errors
            li!= error.msg
   +---- genre_list.pug

    extends layout

    block content
      h1= title

      ul
      each val in list_genres
        li
          a(href=val.url) #{val.name}

      else
        li There are no genre.
    
   +---- index.pug

    extends layout

    block content
      h1= title
      p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network.


      h1 Dynamic content

      if error
        p Error getting dynamic content.
      else

        p The library has the following record counts:

        ul
          li #[strong Books:] !{data.book_count}
          li #[strong Copies:] !{data.book_instance_count}
          li #[strong Copies available:] !{data.book_instance_available_count}
          li #[strong Authors:] !{data.author_count}
          li #[strong Genres:] !{data.genre_count}
   +---- layout.pug

    doctype html
    html(lang='en')
      head
        title= title
        meta(charset='utf-8')
        meta(name='viewport', content='width=device-width, initial-scale=1')
        link(rel="stylesheet", href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css')
        script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js')
        script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js')
        link(rel='stylesheet', href='/stylesheets/style.css')
      body
        div(class='container-fluid')
          div(class='row')
            div(class='col-sm-2')
              block sidebar
                ul(class='sidebar-nav')
                  li
                    a(href='/catalog') Home
                  li
                    a(href='/catalog/books') All books
                  li
                    a(href='/catalog/authors') All authors
                  li
                    a(href='/catalog/genres') All genres
                  li
                    a(href='/catalog/bookinstances') All book-instances
                  li
                    hr
                  li
                    a(href='/catalog/author/create') Create new author
                  li
                    a(href='/catalog/genre/create') Create new genre
                  li
                    a(href='/catalog/book/create') Create new book
                  li
                    a(href='/catalog/bookinstance/create') Create new book instance (copy)

            div(class='col-sm-10')
              block content
    
   BOILERPLATE FOR NODE WEB APIs
   .env

      
   .gitignore

      # Logs
      *.log

      # Node
      node_modules/

      # Project specific
      config/database.js

      # Unit test / coverage reports
      coverage/
      .nyc_output

      # OS auto-generated files
      .DS_Store
      ._*


      # Vim
      *~
      *.swp
      *.swo

      # IDE files
      .idea/
      
   README.md

      # Node API boilerplate

      An opinionated boilerplate for Node web APIs focused on separation of concerns and scalability.

      ## Features

      <dl>
        <dt>Multilayer folder structure</dt>
        <dd>
          <a href="https://github.com/talyssonoc/node-api-boilerplate/wiki/Folder-structure">Code organization</a> inspired by <a href="http://dddcommunity.org/">DDD</a> and <a href="https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html">Clean Architecture</a> focused on codebase scalability.
        </dd>

        <dt>Instant feedback and reload</dt>
        <dd>
          Use <a href="https://www.npmjs.com/package/nodemon">Nodemon</a> to automatically reload the server after a file change when on development mode, makes the development faster and easier.
        </dd>

        <dt>Ready for production</dt>
        <dd>
          Setup with <a href="https://www.npmjs.com/package/pm2">PM2</a> process manager ready to go live on production. It's also out-of-box ready to be deployed at Heroku, you can read more about it <a href="https://github.com/talyssonoc/node-api-boilerplate/wiki/Setup-in-Heroku">here</a>.
        </dd>

        <dt>Scalable and easy to use web server</dt>
        <dd>
          Use <a href="https://www.npmjs.com/package/express">Express</a> for requests routing and middlewares. There are some essential middlewares for web APIs already setup, like <a href="https://www.npmjs.com/package/body-parser">body-parser</a>, <a href="https://www.npmjs.com/package/compression">compression</a>, <a href="https://www.npmjs.com/package/cors">CORS</a> and <a href="https://www.npmjs.com/package/method-override">method-override</a>.
        </dd>

        <dt>Database integration</dt>
        <dd>
          <a href="https://www.npmjs.com/package/sequelize">Sequelize</a>, an ORM for SQL databases, is already integrated, you just have to set the <a href="https://github.com/talyssonoc/node-api-boilerplate/wiki/Database-setup">authentication configurations</a>.
        </dd>

        <dt>Prepared for testing</dt>
        <dd>
          The test suite uses <a href="https://www.npmjs.com/package/mocha">Mocha</a>/<a href="https://www.npmjs.com/package/chai">Chai</a> and is prepared to run unit, integration and functional tests right from the beginning. There are helpers to <a href="https://github.com/talyssonoc/node-api-boilerplate/wiki/The-test-suite">make it easy to make requests to the web app during the tests and for cleaning the database after each test</a>. A <a href="https://www.npmjs.com/package/factory-girl">FactoryGirl</a> adapter for Sequelize is setup to make your tests DRY as well, and the tests generate code coverage measurement with <a href="https://www.npmjs.com/package/istanbul">Istanbul</a>. You should read about the <a href="https://github.com/talyssonoc/node-api-boilerplate/wiki/Chai-plugins">Chai plugins that are setup by default too</a>.
        </dd>

        <dt>Dependency injection</dt>
        <dd>
          With <a href="https://www.npmjs.com/package/awilix">Awilix</a>, a practical dependency injection library, the code will not be coupled and it'll still be easy to resolve automatically the dependencies on the runtime and mock them during the tests. It's even possible inject dependencies on your controllers with the <a href="https://www.npmjs.com/package/awilix-express">Awilix Express adapter</a>. Click <a href="https://github.com/talyssonoc/node-api-boilerplate/wiki/Dependency-injection-container">here</a> if you want to read more about how to use dependency injection with this boilerplate.
        </dd>

        <dt>CLI integration</dt>
        <dd>
          Both the application and Sequelize have command-line tools to make it easy to work with them. Check the <a href="#scripts">Scripts</a> section to know more about this feature.
        </dd>

        <dt>Logging</dt>
        <dd>
          The <a href="https://www.npmjs.com/package/log4js">Log4js</a> logger is highly pluggable, being able to append the messages to a file during the development and send them to a logging service when on production. Even the requests (through <a href="https://www.npmjs.com/package/morgan">morgan</a>) and queries will be logged.
        </dd>

        <dt>Linter</dt>
        <dd>
          It's also setup with <a href="https://www.npmjs.com/package/eslint">ESLint</a> to make it easy to ensure a code styling and find code smells.
        </dd>
      </dl>

      ## Quick start

      _Notice that the boilerplate comes with a small application for user management already, you can delete it with a npm script after you understand how the boilerplate works but please do the quick start first!_ 😊

      1. Clone the repository with `git clone --depth=1 https://github.com/talyssonoc/node-api-boilerplate`
      2. Setup the database on `config/database.js` (there's an example file there to be used with PostgreSQL 😉 )
      3. Install the dependencies with `yarn` (click here if [you don't have Yarn installed](https://yarnpkg.com/docs/install))
      4. Create the development and test databases you have setup on `config/database.js`
      5. Run the database migrations with `npm run sequelize db:migrate`
      6. Add some seed data to the development database with `npm run sequelize db:seed:all`
      7. Run the application in development mode with `npm run dev`
      8. Access `http://localhost:3000/api/users` and you're ready to go!

      After playing a little bit with the boilerplate and _before_ implementing a real application with it I recommend you to read at least the `Setup` and the `Organization and architecture` sections of our [Wiki](https://github.com/talyssonoc/node-api-boilerplate/wiki). After that you'll be able to remove the example application files running `npm run cleanup`

      ## Aditional info:

      - Don't forget to run the migrations for the test environment as well (including when you create a new migration) with `npm run sequelize db:migrate -- --env=test`

      ## Scripts

      This boilerplate comes with a collection of npm scripts to make your life easier, you'll run them with `npm run <script name>` or `yarn run <script name>`:

      - `dev`: Run the application in development mode
      - `start` Run the application in production mode (prefer not to do that in development)
      - `test`: Run the test suite
      - `test:unit`: Run only the unit tests
      - `test:features`: Run only the features tests
      - `coverage`: Run only the unit tests and generate code coverage for them, the output will be on `coverage` folder
      - `lint`: Lint the codebase
      - `sequelize`: Alias to the [Sequelize CLI](https://github.com/sequelize/cli)
      - `console`: Open the built-in console, you can access the DI container through the `container` variable once it's open, the console is promise-friendly. Click [here](https://github.com/talyssonoc/node-api-boilerplate/wiki/Application-console) to know more about the built-in console
      - `cleanup`: Removes the files from the example application

      ## Tech

      - [Node v7.6+](http://nodejs.org/)
      - [Express](https://npmjs.com/package/express)
      - [Sequelize](https://www.npmjs.com/package/sequelize)
      - [Awilix](https://www.npmjs.com/package/awilix)
      - [Structure](https://www.npmjs.com/package/structure)
      - [HTTP Status](https://www.npmjs.com/package/http-status)
      - [Log4js](https://www.npmjs.com/package/log4js)
      - [Morgan](https://www.npmjs.com/package/morgan)
      - [Express Status Monitor](https://www.npmjs.com/package/express-status-monitor)
      - [Nodemon](https://www.npmjs.com/package/nodemon)
      - [PM2](https://www.npmjs.com/package/pm2)
      - [Mocha](https://www.npmjs.com/package/mocha)
      - [Chai](https://www.npmjs.com/package/chai)
      - [FactoryGirl](https://www.npmjs.com/package/factory-girl)
      - [Istanbul](https://www.npmjs.com/package/istanbul) + [NYC](https://www.npmjs.com/package/nyc)
      - [ESLint](https://www.npmjs.com/package/eslint)

      ## Contributing

      This boilerplate is open to suggestions and contributions, documentation contributions are also important! :)
      
   cluster.js

      /* eslint-disable no-console */
      const pm2 = require('pm2');

      const instances = process.env.WEB_CONCURRENCY || -1;
      const maxMemory = process.env.WEB_MEMORY || 512;

      pm2.connect(() => {
        pm2.start({
          script: 'index.js',
          instances: instances,
          max_memory_restart: `${maxMemory}M`,
          env: {
            NODE_ENV: 'production',
            NODE_PATH: '.'
          },
        }, (err) => {
          if (err) {
            return console.error('Error while launching applications', err.stack || err);
          }

          console.log('PM2 and application has been succesfully started');

          pm2.launchBus((err, bus) => {
            console.log('[PM2] Log streaming started');

            bus.on('log:out', (packet) => {
              console.log('[App:%s] %s', packet.process.name, packet.data);
            });

            bus.on('log:err', (packet) => {
              console.error('[App:%s][Err] %s', packet.process.name, packet.data);
            });
          });
        });
      });
      
   config
   +---- database.js.example

      module.exports = {
        development: {
          username: 'root',
          password: null,
          database: 'boilerplate_development',
          host: '127.0.0.1',
          dialect: 'postgres'
        },
        test: {
          username: 'root',
          password: null,
          database: 'boilerplate_test',
          host: '127.0.0.1',
          dialect: 'postgres',
          logging: null
        },
        production: process.env.DATABASE_URL
      };
      
   +---- environments
   |      +---- development.js

      const path = require('path');
      const logPath = path.join(__dirname, '../../logs/development.log');

      module.exports = {
        web: {
          port: 3000
        },
        logging: {
          appenders: [
            { type: 'console' },
            { type: 'file', filename: logPath }
          ]
        }
      };
      
   |      +---- production.js

      module.exports = {
        web: {
          port: process.env.PORT
        },
        logging: {
          appenders: [
            { type: 'console', layout: { type: 'basic' } }
          ]
        }
      };
      
   |      +---- test.js

      module.exports = {
        web: {}
      };
      
   +---- index.js

      require('dotenv').load();

      const fs = require('fs');
      const path = require('path');

      const ENV = process.env.NODE_ENV || 'development';

      const envConfig = require(path.join(__dirname, 'environments', ENV));
      const dbConfig = loadDbConfig();

      const config = Object.assign({
        [ENV]: true,
        env: ENV,
        db: dbConfig
      }, envConfig);

      module.exports = config;

      function loadDbConfig() {
        if(process.env.DATABASE_URL) {
          return process.env.DATABASE_URL;
        }

        if(fs.existsSync(path.join(__dirname, './database.js'))) {
          return require('./database')[ENV];
        }
      }
      
   index.js

      const container = require('src/container');

      const app = container.resolve('app');

      app
        .start()
        .catch((error) => {
          app.logger.error(error.stack);
          process.exit();
        });
      
   logs
   package.json

      {
        "name": "node-api-boilerplate",
        "version": "1.1.1",
        "description": "A boilerplate for web APIs using Node",
        "main": "index.js",
        "private": true,
        "engines": {
          "node": ">=7.6.0"
        },
        "scripts": {
          "start": "node cluster.js",
          "dev": "cross-env NODE_PATH=. NODE_ENV=development nodemon",
          "test": "npm run test:all",
          "test:all": "npm run test:unit && npm run test:features",
          "test:unit": "cross-env NODE_PATH=. NODE_ENV=test mocha --opts test/mocha.opts.unit",
          "test:features": "cross-env NODE_PATH=. NODE_ENV=test mocha --opts test/mocha.opts.features",
          "coverage": "cross-env NODE_PATH=. NODE_ENV=test nyc mocha --opts test/mocha.opts.unit",
          "lint": "eslint {src,test,config}/**/*.js",
          "sequelize": "cross-env NODE_PATH=. sequelize",
          "console": "cross-env NODE_PATH=. node src/interfaces/console/index.js",
          "heroku-postbuild": "NODE_ENV=production NODE_PATH=. sequelize db:migrate --url=$DATABASE_URL",
          "pm2": "pm2",
          "cleanup": "node scripts/cleanup.js"
        },
        "repository": {},
        "author": "Talysson <talyssonoc@gmail.com>",
        "license": "MIT",
        "dependencies": {
          "awilix": "^3.0.9",
          "awilix-express": "^0.11.0",
          "body-parser": "^1.17.1",
          "compression": "^1.6.2",
          "cors": "^2.8.1",
          "cross-env": "^3.2.3",
          "del": "^2.2.2",
          "dotenv": "^4.0.0",
          "eslint": "^4.7.2",
          "express": "^4.15.2",
          "express-status-monitor": "^0.1.9",
          "http-status": "^1.0.1",
          "log4js": "^1.1.1",
          "method-override": "^2.3.7",
          "morgan": "^1.8.1",
          "pg": "^6.1.3",
          "pm2": "^2.4.2",
          "sequelize": "^3.30.4",
          "sequelize-cli": "^3.0.0",
          "structure": "^1.2.0",
          "swagger-ui-express": "^2.0.14"
        },
        "devDependencies": {
          "chai": "^4.1.2",
          "chai-change": "^2.1.2",
          "chance": "^1.0.6",
          "dirty-chai": "^2.0.1",
          "factory-girl": "^4.0.0",
          "listr": "^0.11.0",
          "mocha": "^3.2.0",
          "nodemon": "^1.11.0",
          "nyc": "^11.2.1",
          "replace-in-file": "^2.5.0",
          "supertest": "^3.0.0",
          "supertest-as-promised": "^4.0.2"
        }
      }
      
   scripts
   +---- cleanup.js

      const path = require('path');
      const replace = require('replace-in-file');
      const remove = require('del');
      const Listr = require('listr');
      const { writeFileSync } = require('fs');

      const srcPath = path.join(__dirname, '..', 'src');
      const testPath = path.join(__dirname, '..', 'test');
      const srcAndTestPath = `{${testPath}/unit,${srcPath}}`;
      const routerPath = path.join(srcPath, 'interfaces', 'http', 'router.js');
      const containerPath = path.join(srcPath, 'container.js');

      const tasks = new Listr([
        {
          title: 'Remove UsersController routes',
          task() {
            return replace({
              files: routerPath,
              from: /\s*apiRouter.*UsersController'\)\);/,
              to: ''
            });
          }
        },
        {
          title: 'Remove example files from DI container',
          task() {
            return replace({
              files: containerPath,
              from: [
                /\s*const \{(\n.*)+.*app\/user'\);/,
                /\s*const.*UsersRepository'\);/,
                /\s*const.*UserSerializer'\);/,
                /\, User: UserModel/,
                /\s*usersRepository.*\}\]/,
                /\,\s*UserModel/,
                /\s+createUser(.|\n)+.*DeleteUser\n/,
                /\s+userSerializer: UserSerializer\n/
              ],
              to: ''
            });
          }
        },
        {
          title: 'Delete example files and tests',
          task() {
            return remove([
              path.join(srcAndTestPath, 'app', 'user', '**'),
              path.join(srcAndTestPath, 'domain', 'user', '**'),
              path.join(srcAndTestPath, 'infra', 'user', '**'),
              path.join(srcAndTestPath, 'interfaces', 'http', 'user', '**'),
              path.join(srcPath, 'infra', 'database', 'migrate', '*.js'),
              path.join(srcPath, 'infra', 'database', 'seeds', '*.js'),
              path.join(srcPath, 'infra', 'database', 'models', 'User.js'),
              path.join(testPath, 'features', 'api', 'users', '**'),
              path.join(testPath, 'support', 'factories', '*.js')
            ]);
          }
        },
        {
          title: 'Remove example data from swagger.json',
          task() {
            writeFileSync(
              path.join(srcPath, 'interfaces', 'http', 'swagger', 'swagger.json'),
              JSON.stringify({
                openapi: '3.0.0',
                info: {
                  title: 'Node API boilerplate',
                  version: 'v1'
                },
                servers: [
                  {
                    description: 'Local server',
                    url: '/api'
                  }
                ]
              }, null, '  ')
            );
          }
        },
        {
          title: 'Remove cleanup script from package.json',
          task() {
            return replace({
              files: path.join(__dirname, '..', 'package.json'),
              from: /\,\s*\"cleanup.*cleanup\.js\"/,
              to: ''
            });
          }
        }
      ]);

      tasks.run().catch((err) => {
        console.error(err);
      });
      
   src
   +---- app
   |      +---- Application.js

      class Application {
        constructor({ server, database, logger }) {
          this.server = server;
          this.database = database;
          this.logger = logger;

          if(database && database.options.logging) {
            database.options.logging = logger.info.bind(logger);
          }
        }

        async start() {
          if(this.database) {
            await this.database.authenticate();
          }

          await this.server.start();
        }
      }

      module.exports = Application;
      
   |      +---- Operation.js

      const EventEmitter = require('events');
      const define = Object.defineProperty;

      class Operation extends EventEmitter {
        static setOutputs(outputs) {
          define(this.prototype, 'outputs', {
            value: createOutputs(outputs)
          });
        }

        on(output, handler) {
          if(this.outputs[output]) {
            return this.addListener(output, handler);
          }

          throw new Error(`Invalid output "${output}" to operation ${this.constructor.name}.`);
        }
      }

      const createOutputs = (outputsArray) => {
        return outputsArray.reduce((obj, output) => {
          obj[output] = output;
          return obj;
        }, Object.create(null));
      };

      module.exports = Operation;
      
   |      +---- user
   |            +---- CreateUser.js

      const Operation = require('src/app/Operation');
      const User = require('src/domain/user/User');

      class CreateUser extends Operation {
        constructor({ usersRepository }) {
          super();
          this.usersRepository = usersRepository;
        }

        async execute(userData) {
          const { SUCCESS, ERROR, VALIDATION_ERROR } = this.outputs;

          const user = new User(userData);

          try {
            const newUser = await this.usersRepository.add(user);

            this.emit(SUCCESS, newUser);
          } catch(error) {
            if(error.message === 'ValidationError') {
              return this.emit(VALIDATION_ERROR, error);
            }

            this.emit(ERROR, error);
          }
        }
      }

      CreateUser.setOutputs(['SUCCESS', 'ERROR', 'VALIDATION_ERROR']);

      module.exports = CreateUser;
      
   |            +---- DeleteUser.js

      const Operation = require('src/app/Operation');

      class DeleteUser extends Operation {
        constructor({ usersRepository }) {
          super();
          this.usersRepository = usersRepository;
        }

        async execute(userId) {
          const { SUCCESS, ERROR, NOT_FOUND } = this.outputs;

          try {
            await this.usersRepository.remove(userId);
            this.emit(SUCCESS);
          } catch(error) {
            if(error.message === 'NotFoundError') {
              return this.emit(NOT_FOUND, error);
            }

            this.emit(ERROR, error);
          }
        }
      }

      DeleteUser.setOutputs(['SUCCESS', 'ERROR', 'NOT_FOUND']);

      module.exports = DeleteUser;
      
   |            +---- GetAllUsers.js

      const Operation = require('src/app/Operation');

      class GetAllUsers extends Operation {
        constructor({ usersRepository }) {
          super();
          this.usersRepository = usersRepository;
        }

        async execute() {
          const { SUCCESS, ERROR } = this.outputs;

          try {
            const users = await this.usersRepository.getAll({
              attributes: ['id', 'name']
            });

            this.emit(SUCCESS, users);
          } catch(error) {
            this.emit(ERROR, error);
          }
        }
      }

      GetAllUsers.setOutputs(['SUCCESS', 'ERROR']);

      module.exports = GetAllUsers;
      
   |            +---- GetUser.js

      const Operation = require('src/app/Operation');

      class GetUser extends Operation {
        constructor({ usersRepository }) {
          super();
          this.usersRepository = usersRepository;
        }

        async execute(userId) {
          const { SUCCESS, NOT_FOUND } = this.outputs;

          try {
            const user = await this.usersRepository.getById(userId);
            this.emit(SUCCESS, user);
          } catch(error) {
            this.emit(NOT_FOUND, {
              type: error.message,
              details: error.details
            });
          }
        }
      }

      GetUser.setOutputs(['SUCCESS', 'ERROR', 'NOT_FOUND']);

      module.exports = GetUser;
      
   |            +---- UpdateUser.js

      const Operation = require('src/app/Operation');

      class UpdateUser extends Operation {
        constructor({ usersRepository }) {
          super();
          this.usersRepository = usersRepository;
        }

        async execute(userId, userData) {
          const {
            SUCCESS, NOT_FOUND, VALIDATION_ERROR, ERROR
          } = this.outputs;

          try {
            const user = await this.usersRepository.update(userId, userData);
            this.emit(SUCCESS, user);
          } catch(error) {
            switch(error.message) {
            case 'ValidationError':
              return this.emit(VALIDATION_ERROR, error);
            case 'NotFoundError':
              return this.emit(NOT_FOUND, error);
            default:
              this.emit(ERROR, error);
            }
          }
        }
      }

      UpdateUser.setOutputs(['SUCCESS', 'NOT_FOUND', 'VALIDATION_ERROR', 'ERROR']);

      module.exports = UpdateUser;
      
   |            +---- index.js

      module.exports = {
        GetAllUsers: require('./GetAllUsers'),
        CreateUser: require('./CreateUser'),
        GetUser: require('./GetUser'),
        UpdateUser: require('./UpdateUser'),
        DeleteUser: require('./DeleteUser')
      };
      
   +---- container.js

      const { createContainer, asClass, asFunction, asValue } = require('awilix');
      const { scopePerRequest } = require('awilix-express');

      const config = require('../config');
      const Application = require('./app/Application');
      const {
        CreateUser,
        GetAllUsers,
        GetUser,
        UpdateUser,
        DeleteUser
      } = require('./app/user');

      const UserSerializer = require('./interfaces/http/user/UserSerializer');

      const Server = require('./interfaces/http/Server');
      const router = require('./interfaces/http/router');
      const loggerMiddleware = require('./interfaces/http/logging/loggerMiddleware');
      const errorHandler = require('./interfaces/http/errors/errorHandler');
      const devErrorHandler = require('./interfaces/http/errors/devErrorHandler');
      const swaggerMiddleware = require('./interfaces/http/swagger/swaggerMiddleware');

      const logger = require('./infra/logging/logger');
      const SequelizeUsersRepository = require('./infra/user/SequelizeUsersRepository');
      const { database, User: UserModel } = require('./infra/database/models');

      const container = createContainer();

      // System
      container
        .register({
          app: asClass(Application).singleton(),
          server: asClass(Server).singleton()
        })
        .register({
          router: asFunction(router).singleton(),
          logger: asFunction(logger).singleton()
        })
        .register({
          config: asValue(config)
        });

      // Middlewares
      container
        .register({
          loggerMiddleware: asFunction(loggerMiddleware).singleton()
        })
        .register({
          containerMiddleware: asValue(scopePerRequest(container)),
          errorHandler: asValue(config.production ? errorHandler : devErrorHandler),
          swaggerMiddleware: asValue([swaggerMiddleware])
        });

      // Repositories
      container.register({
        usersRepository: asClass(SequelizeUsersRepository).singleton()
      });

      // Database
      container.register({
        database: asValue(database),
        UserModel: asValue(UserModel)
      });

      // Operations
      container.register({
        createUser: asClass(CreateUser),
        getAllUsers: asClass(GetAllUsers),
        getUser: asClass(GetUser),
        updateUser: asClass(UpdateUser),
        deleteUser: asClass(DeleteUser)
      });

      // Serializers
      container.register({
        userSerializer: asValue(UserSerializer)
      });

      module.exports = container;
      
   +---- domain
   |      +---- user
   |            +---- User.js

      const { attributes } = require('structure');

      const User = attributes({
        id: Number,
        name: {
          type: String,
          required: true
        },
        age: Number
      })(class User {
        isLegal() {
          return this.age >= User.MIN_LEGAL_AGE;
        }
      });

      User.MIN_LEGAL_AGE = 21;

      module.exports = User;
      
   +---- infra
   |      +---- database
   |            +---- migrate
   |                  +---- 20170308131539-create-user.js

      'use strict';
      module.exports = {
        up: function(queryInterface, Sequelize) {
          return queryInterface.createTable('users', {
            id: {
              allowNull: false,
              autoIncrement: true,
              primaryKey: true,
              type: Sequelize.INTEGER
            },
            name: {
              type: Sequelize.STRING
            },
            createdAt: {
              allowNull: false,
              type: Sequelize.DATE
            },
            updatedAt: {
              allowNull: false,
              type: Sequelize.DATE
            }
          });
        },
        down: function(queryInterface) {
          return queryInterface.dropTable('users');
        }
      };
      
   |            +---- models
   |                  +---- User.js

      'use strict';

      module.exports = function(sequelize, DataTypes) {
        const User = sequelize.define('user', {
          name: DataTypes.STRING
        }, {
          classMethods: {
            associate() {
              // associations can be defined here
            }
          }
        });

        return User;
      };
      
   |                  +---- index.js

      const { ModelsLoader } = require('src/infra/sequelize');
      const Sequelize = require('sequelize');
      const { db: config } = require('config');

      if(config) {
        const sequelize = new Sequelize(config);

        module.exports = ModelsLoader.load({
          sequelize,
          baseFolder: __dirname
        });
      } else {
        /* eslint-disable no-console */
        console.error('Database configuration not found, disabling database.');
        /* eslint-enable no-console */
      }

      
   |            +---- seeds
   |                  +---- 20170308131757-test-users.js

      'use strict';

      const dataFaker = require('src/infra/support/dataFaker');

      module.exports = {
        up: function (queryInterface) {
          const testUsers = [];

          for(let i = 0; i < 20; i++) {
            testUsers.push({
              name: dataFaker.name(),
              createdAt: new Date(),
              updatedAt: new Date()
            });
          }

          return queryInterface.bulkInsert('users', testUsers, {});
        },

        down: function (queryInterface) {
          return queryInterface.bulkDelete('users', null, {});
        }
      };
      
   |      +---- factoryGirl
   |            +---- FactoriesLoader.js

      const fs = require('fs');
      const path = require('path');

      module.exports = {
        load({ factoryGirl, baseFolder, models }) {
          fs
            .readdirSync(baseFolder)
            .filter((file) => {
              return (file.indexOf('.') !== 0) && (file.slice(-3) === '.js');
            })
            .forEach((file) => {
              const factoryPath = path.join(baseFolder, file);
              const factory = require(factoryPath);

              factory(factoryGirl, models);
            });

          return factoryGirl;
        }
      };
      
   |            +---- index.js

      module.exports = {
        FactoriesLoader: require('./FactoriesLoader')
      };
      
   |      +---- logging
   |            +---- LoggerStreamAdapter.js

      const LoggerStreamAdapter = {
        toStream(logger) {
          return {
            write(message) {
              logger.info(message.slice(0, -1));
            }
          };
        }
      };

      module.exports = LoggerStreamAdapter;
      
   |            +---- logger.js

      const Log4js = require('log4js');

      module.exports = ({ config }) => {
        Log4js.configure(config.logging);

        return Log4js.getLogger();
      };
      
   |      +---- sequelize
   |            +---- ModelsLoader.js

      const fs = require('fs');
      const path = require('path');

      module.exports = {
        load({ sequelize, baseFolder, indexFile = 'index.js' }) {
          const loaded = {};

          fs
            .readdirSync(baseFolder)
            .filter((file) => {
              return (file.indexOf('.') !== 0) && (file !== indexFile) && (file.slice(-3) === '.js');
            })
            .forEach((file) => {
              const model = sequelize['import'](path.join(baseFolder, file));
              const modelName = file.split('.')[0];
              loaded[modelName] = model;
            });

          Object.keys(loaded).forEach((modelName) => {
            if(loaded[modelName].associate) {
              loaded[modelName].associate(loaded);
            }
          });

          loaded.database = sequelize;

          return loaded;
        }
      };
      
   |            +---- index.js

      module.exports = {
        ModelsLoader: require('./ModelsLoader')
      };
      
   |      +---- support
   |            +---- dataFaker.js

      const Chance = require('chance');
      const chance = new Chance();

      module.exports = chance;
      
   |      +---- user
   |            +---- SequelizeUserMapper.js

      const User = require('src/domain/user/User');

      const SequelizeUserMapper = {
        toEntity({ dataValues }) {
          const { id, name } = dataValues;

          return new User({ id, name });
        },

        toDatabase(survivor) {
          const { name } = survivor;

          return { name };
        }
      };

      module.exports = SequelizeUserMapper;
      
   |            +---- SequelizeUsersRepository.js

      const UserMapper = require('./SequelizeUserMapper');

      class SequelizeUsersRepository {
        constructor({ UserModel }) {
          this.UserModel = UserModel;
        }

        async getAll(...args) {
          const users = await this.UserModel.findAll(...args);

          return users.map(UserMapper.toEntity);
        }

        async getById(id) {
          const user = await this._getById(id);

          return UserMapper.toEntity(user);
        }

        async add(user) {
          const { valid, errors } = user.validate();

          if(!valid) {
            const error = new Error('ValidationError');
            error.details = errors;

            throw error;
          }

          const newUser = await this.UserModel.create(UserMapper.toDatabase(user));
          return UserMapper.toEntity(newUser);
        }

        async remove(id) {
          const user = await this._getById(id);

          await user.destroy();
          return;
        }

        async update(id, newData) {
          const user = await this._getById(id);

          const transaction = await this.UserModel.sequelize.transaction();

          try {
            const updatedUser = await user.update(newData, { transaction });
            const userEntity = UserMapper.toEntity(updatedUser);

            const { valid, errors } = userEntity.validate();

            if(!valid) {
              const error = new Error('ValidationError');
              error.details = errors;

              throw error;
            }

            await transaction.commit();

            return userEntity;
          } catch(error) {
            await transaction.rollback();

            throw error;
          }
        }

        async count() {
          return await this.UserModel.count();
        }

        // Private

        async _getById(id) {
          try {
            return await this.UserModel.findById(id, { rejectOnEmpty: true });
          } catch(error) {
            if(error.name === 'SequelizeEmptyResultError') {
              const notFoundError = new Error('NotFoundError');
              notFoundError.details = `User with id ${id} can't be found.`;

              throw notFoundError;
            }

            throw error;
          }
        }
      }

      module.exports = SequelizeUsersRepository;
      
   +---- interfaces
   |      +---- console
   |            +---- Console.js

      const REPL = require('repl');
      const vm = require('vm');

      module.exports = {
        start(options = {}) {
          const { expose } = options;

          const repl = REPL.start({
            eval: promisableEval
          });

          Object.assign(repl.context, expose);
        }
      };


      function promisableEval(cmd, context, filename, callback) {
        const result = vm.runInContext(cmd, context);

        if(isPromise(result)) {
          return result
            .then((v) => callback(null, v))
            .catch((e) => callback(e));
        }

        return callback(null, result);
      }

      function isPromise(value) {
        return value
        && (typeof value.then === 'function')
        && (typeof value.catch === 'function');
      }
      
   |            +---- index.js

      process.env.NODE_ENV = process.env.NODE_ENV || 'development';

      const Console = require('./Console');
      const container = require('src/container');

      Console.start({
        expose: { container }
      });
      
   |      +---- http
   |            +---- Server.js

      const express = require('express');

      class Server {
        constructor({ config, router, logger }) {
          this.config = config;
          this.logger = logger;
          this.express = express();

          this.express.disable('x-powered-by');
          this.express.use(router);
        }

        start() {
          return new Promise((resolve) => {
            const http = this.express
              .listen(this.config.web.port, () => {
                const { port } = http.address();
                this.logger.info(`[p ${process.pid}] Listening at port ${port}`);
                resolve();
              });
          });
        }
      }

      module.exports = Server;
      
   |            +---- errors
   |                  +---- devErrorHandler.js

      const Status = require('http-status');

      /* istanbul ignore next */
      module.exports = (err, req, res, next) => { // eslint-disable-line no-unused-vars
        const { logger } = req.container.cradle;

        logger.error(err);

        res.status(Status.INTERNAL_SERVER_ERROR).json({
          type: 'InternalServerError',
          message: err.message,
          stack: err.stack
        });
      };
      
   |                  +---- errorHandler.js

      const Status = require('http-status');

      /* istanbul ignore next */
      module.exports = (err, req, res, next) => { // eslint-disable-line no-unused-vars
        const { logger } = req.container.cradle;

        logger.error(err);

        res.status(Status.INTERNAL_SERVER_ERROR).json({
          type: 'InternalServerError',
          message: 'The server failed to handle this request'
        });
      };
      
   |            +---- logging
   |                  +---- loggerMiddleware.js

      const morgan = require('morgan');
      const LoggerStreamAdapter = require('src/infra/logging/LoggerStreamAdapter');

      module.exports = ({ logger }) => {
        return morgan('dev', {
          stream: LoggerStreamAdapter.toStream(logger)
        });
      };
      
   |            +---- router.js

      const { Router } = require('express');
      const statusMonitor = require('express-status-monitor');
      const cors = require('cors');
      const bodyParser = require('body-parser');
      const compression = require('compression');
      const methodOverride = require('method-override');
      const controller = require('./utils/createControllerRoutes');

      module.exports = ({ config, containerMiddleware, loggerMiddleware, errorHandler, swaggerMiddleware }) => {
        const router = Router();

        /* istanbul ignore if */
        if(config.env === 'development') {
          router.use(statusMonitor());
        }

        /* istanbul ignore if */
        if(config.env !== 'test') {
          router.use(loggerMiddleware);
        }

        const apiRouter = Router();

        apiRouter
          .use(methodOverride('X-HTTP-Method-Override'))
          .use(cors())
          .use(bodyParser.json())
          .use(compression())
          .use(containerMiddleware)
          .use('/docs', swaggerMiddleware);

        /*
        * Add your API routes here
        *
        * You can use the `controllers` helper like this:
        * apiRouter.use('/users', controller(controllerPath))
        *
        * The `controllerPath` is relative to the `interfaces/http` folder
        */

        apiRouter.use('/users', controller('user/UsersController'));

        router.use('/api', apiRouter);

        router.use(errorHandler);

        return router;
      };
      
   |            +---- swagger
   |                  +---- swagger.json

      {
        "openapi": "3.0.0",
        "info": {
          "title": "Node API boilerplate",
          "version": "v1"
        },
        "servers": [
          {
            "description": "Local server",
            "url": "/api"
          }
        ],
        "paths": {
          "/users": {
            "get": {
              "operationId": "listUsers",
              "tags": [ "Users" ],
              "responses": {
                "200": {
                  "description": "List of all users",
                  "content": {
                    "application/json": {
                      "schema": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/User"
                        }
                      }
                    }
                  }
                }
              }
            },
            "post": {
              "operationId": "createUser",
              "tags": [ "Users" ],
              "requestBody": {
                "description": "User data",
                "required": true,
                "content": {
                  "application/json": {
                    "schema": {
                      "$ref": "#/components/schemas/NewUser"
                    }
                  }
                }
              },
              "responses": {
                "201": {
                  "description": "User created successfully",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/User"
                      }
                    }
                  }
                },
                "400": {
                  "description": "User not created because of validation error",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/ValidationError"
                      }
                    }
                  }
                }
              }
            }
          },
          "/users/{id}": {
            "get": {
              "operationId": "showUser",
              "tags": [ "Users" ],
              "parameters": [
                {
                  "name": "id",
                  "in": "path",
                  "description": "Id of user to show",
                  "required": true,
                  "type": "integer",
                  "format": "int64"
                }
              ],
              "responses": {
                "200": {
                  "description": "Return user with given id",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/User"
                      }
                    }
                  }
                },
                "404": {
                  "description": "User not found",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/NotFoundError"
                      }
                    }
                  }
                }
              }
            },
            "put": {
              "operationId": "updateUser",
              "tags": [ "Users" ],
              "parameters": [
                {
                  "name": "id",
                  "in": "path",
                  "description": "Id of user to update",
                  "required": true,
                  "type": "integer",
                  "format": "int64"
                }
              ],
              "requestBody": {
                "description": "User new data",
                "required": true,
                "content": {
                  "application/json": {
                    "schema": {
                      "$ref": "#/components/schemas/NewUser"
                    }
                  }
                }
              },
              "responses": {
                "202": {
                  "description": "User updated successfully",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/User"
                      }
                    }
                  }
                },
                "404": {
                  "description": "User not found",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/NotFoundError"
                      }
                    }
                  }
                }
              }
            },
            "delete": {
              "operationId": "deleteUser",
              "tags": [ "Users" ],
              "parameters": [
                {
                  "name": "id",
                  "in": "path",
                  "description": "Id of user to delete",
                  "required": true,
                  "type": "integer",
                  "format": "int64"
                }
              ],
              "responses": {
                "202": {
                  "description": "User deleted successfully"
                },
                "404": {
                  "description": "User not found",
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/NotFoundError"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "components": {
          "schemas": {
            "User": {
              "allOf": [
                { "$ref": "#/components/schemas/NewUser" },
                {
                  "required": [ "id" ],
                  "properties": {
                    "id": {
                      "type": "integer",
                      "format": "int64"
                    }
                  }
                }
              ]
            },
            "NewUser": {
              "required": [ "name" ],
              "properties": {
                "name": {
                  "type": "string"
                }
              }
            },
            "ValidationError": {
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [ "ValidationError" ]
                },
                "details": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ValidationErrorDetail"
                  }
                }
              }
            },
            "ValidationErrorDetail": {
              "properties": {
                "message": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                }
              }
            },
            "NotFoundError": {
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [ "NotFoundError" ]
                },
                "details": {
                  "type": "string",
                  "enum": [ "User with id {id} not found" ]
                }
              }
            }
          }
        }
      }
      
   |                  +---- swaggerMiddleware.js

      const SwaggerUi = require('swagger-ui-express');
      const swaggerDocument = require('./swagger.json');

      module.exports = [SwaggerUi.serve, SwaggerUi.setup(swaggerDocument)];
      
   |            +---- user
   |                  +---- UserSerializer.js

      const UserSerializer = {
        serialize({ id, name }) {
          return {
            id,
            name
          };
        }
      };

      module.exports = UserSerializer;
      
   |                  +---- UsersController.js

      const { Router } = require('express');
      const { inject } = require('awilix-express');
      const Status = require('http-status');

      const UsersController = {
        get router() {
          const router = Router();

          router.use(inject('userSerializer'));

          router.get('/', inject('getAllUsers'), this.index);
          router.get('/:id', inject('getUser'), this.show);
          router.post('/', inject('createUser'), this.create);
          router.put('/:id', inject('updateUser'), this.update);
          router.delete('/:id', inject('deleteUser'), this.delete);

          return router;
        },

        index(req, res, next) {
          const { getAllUsers, userSerializer } = req;
          const { SUCCESS, ERROR } = getAllUsers.outputs;

          getAllUsers
            .on(SUCCESS, (users) => {
              res
                .status(Status.OK)
                .json(users.map(userSerializer.serialize));
            })
            .on(ERROR, next);

          getAllUsers.execute();
        },

        show(req, res, next) {
          const { getUser, userSerializer } = req;

          const { SUCCESS, ERROR, NOT_FOUND } = getUser.outputs;

          getUser
            .on(SUCCESS, (user) => {
              res
                .status(Status.OK)
                .json(userSerializer.serialize(user));
            })
            .on(NOT_FOUND, (error) => {
              res.status(Status.NOT_FOUND).json({
                type: 'NotFoundError',
                details: error.details
              });
            })
            .on(ERROR, next);

          getUser.execute(Number(req.params.id));
        },

        create(req, res, next) {
          const { createUser, userSerializer } = req;
          const { SUCCESS, ERROR, VALIDATION_ERROR } = createUser.outputs;

          createUser
            .on(SUCCESS, (user) => {
              res
                .status(Status.CREATED)
                .json(userSerializer.serialize(user));
            })
            .on(VALIDATION_ERROR, (error) => {
              res.status(Status.BAD_REQUEST).json({
                type: 'ValidationError',
                details: error.details
              });
            })
            .on(ERROR, next);

          createUser.execute(req.body);
        },

        update(req, res, next) {
          const { updateUser, userSerializer } = req;
          const { SUCCESS, ERROR, VALIDATION_ERROR, NOT_FOUND } = updateUser.outputs;

          updateUser
            .on(SUCCESS, (user) => {
              res
                .status(Status.ACCEPTED)
                .json(userSerializer.serialize(user));
            })
            .on(VALIDATION_ERROR, (error) => {
              res.status(Status.BAD_REQUEST).json({
                type: 'ValidationError',
                details: error.details
              });
            })
            .on(NOT_FOUND, (error) => {
              res.status(Status.NOT_FOUND).json({
                type: 'NotFoundError',
                details: error.details
              });
            })
            .on(ERROR, next);

          updateUser.execute(Number(req.params.id), req.body);
        },

        delete(req, res, next) {
          const { deleteUser } = req;
          const { SUCCESS, ERROR,  NOT_FOUND } = deleteUser.outputs;

          deleteUser
            .on(SUCCESS, () => {
              res.status(Status.ACCEPTED).end();
            })
            .on(NOT_FOUND, (error) => {
              res.status(Status.NOT_FOUND).json({
                type: 'NotFoundError',
                details: error.details
              });
            })
            .on(ERROR, next);

          deleteUser.execute(Number(req.params.id));
        }
      };

      module.exports = UsersController;
      
   |            +---- utils
   |                  +---- createControllerRoutes.js

      const path = require('path');

      module.exports = function createControllerRoutes(controllerUri) {
        const controllerPath = path.resolve('src/interfaces/http', controllerUri);
        const Controller = require(controllerPath);

        return Controller.router;
      };
      
   test
   +---- features
   |      +---- api
   |            +---- users
   |                  +---- createUser.spec.js

      const request = require('test/support/request');
      const { expect } = require('chai');

      describe('API :: POST /api/users', () => {
        context('when sent data is ok', () => {
          it('creates and returns 201 and the new user', async () => {
            const { body } = await request()
              .post('/api/users')
              .send({
                name: 'New User'
              })
              .expect(201);

            expect(body.id).to.exist;
            expect(body.name).to.equal('New User');
            expect(body).to.have.all.keys('id', 'name');
          });
        });

        context('when name is missing', () => {
          it('does not create and returns 400 with the validation error', async () => {
            const { body } = await request()
              .post('/api/users')
              .expect(400);

            expect(body.type).to.equal('ValidationError');
            expect(body.details).to.have.lengthOf(1);
            expect(body.details[0].message).to.equal('"name" is required');
          });
        });
      });
      
   |                  +---- deleteUser.spec.js

      const request = require('test/support/request');
      const factory = require('test/support/factory');
      const { expect } = require('chai');

      describe('API :: DELETE /api/users/:id', () => {
        context('when user exists', () => {
          it('deletes the user and return status 202', async () => {
            const user = await factory.create('user', {
              name: 'User'
            });

            await request()
              .delete(`/api/users/${user.id}`)
              .expect(202);
          });
        });

        context('when user does not exist', () => {
          it('returns the not found message and status 404', async () => {
            const { body } = await request()
              .delete('/api/users/0')
              .send({
                name: 'Updated User'
              })
              .expect(404);

            expect(body.type).to.equal('NotFoundError');
            expect(body.details).to.equal('User with id 0 can\'t be found.');
          });
        });
      });
      
   |                  +---- listUsers.spec.js

      const request = require('test/support/request');
      const factory = require('test/support/factory');
      const { expect } = require('chai');

      describe('API :: GET /api/users', () => {
        context('when there are users', () => {
          beforeEach(() => {
            return factory.createMany('user', 2, [
              { name: 'First' },
              { name: 'Second' }
            ]);
          });

          it('return success with array of users', async () => {
            const { body } = await request()
              .get('/api/users')
              .expect(200);

            expect(body).to.have.lengthOf(2);

            expect(body[0].name).to.equal('First');
            expect(body[0]).to.have.all.keys('id', 'name');

            expect(body[1].name).to.equal('Second');
            expect(body[1]).to.have.all.keys('id', 'name');
          });
        });

        context('when there are no users', () => {
          it('return success with empty array', async () => {
            const { body } = await request()
              .get('/api/users')
              .expect(200);

            expect(body).to.have.lengthOf(0);
          });
        });
      });
      
   |                  +---- showUser.spec.js

      const request = require('test/support/request');
      const factory = require('test/support/factory');
      const { expect } = require('chai');

      describe('API :: GET /api/users/:id', () => {
        context('when user exists', () => {
          it('returns the user and status 200', async () => {
            const user = await factory.create('user', {
              name: 'The User'
            });

            const { body } = await request()
              .get(`/api/users/${user.id}`)
              .expect(200);

            expect(body.id).to.equal(user.id);
            expect(body.name).to.equal('The User');
          });
        });

        context('when user does not exist', () => {
          it('returns a not found error and status 404', async () => {
            const { body } = await request()
              .get('/api/users/0')
              .expect(404);

            expect(body.type).to.equal('NotFoundError');
            expect(body.details).to.equal('User with id 0 can\'t be found.');
          });
        });
      });
      
   |                  +---- updateUser.spec.js

      const request = require('test/support/request');
      const factory = require('test/support/factory');
      const { expect } = require('chai');

      describe('API :: PUT /api/users/:id', () => {
        context('when user exists', () => {
          context('when sent data is ok', () => {
            it('updates and returns 202 with the updated user', async () => {
              const user = await factory.create('user', {
                name: 'User'
              });

              const { body } = await request()
                .put(`/api/users/${user.id}`)
                .send({
                  name: 'Updated User'
                })
                .expect(202);

              expect(body.id).to.equal(user.id);
              expect(body.name).to.equal('Updated User');
            });
          });

          context('when name is empty', () => {
            it('does update and returns 400 with the validation error', async () => {
              const user = await factory.create('user', {
                name: 'User'
              });

              const { body } = await request()
                .put(`/api/users/${user.id}`)
                .send({
                  name: ''
                })
                .expect(400);

              expect(body.type).to.equal('ValidationError');
              expect(body.details).to.have.lengthOf(1);
              expect(body.details[0].message).to.equal('"name" is not allowed to be empty');
            });
          });
        });

        context('when user does not exist', () => {
          it('returns the not found message and status 404', async () => {
            const { body } = await request()
              .put('/api/users/0')
              .send({
                name: 'Updated User'
              })
              .expect(404);

            expect(body.type).to.equal('NotFoundError');
            expect(body.details).to.equal('User with id 0 can\'t be found.');
          });
        });
      });
      
   +---- mocha.opts.features

      test/setup.js
      test/features/**/*.spec.js
      --recursive
      
   +---- mocha.opts.unit

      test/setup.js
      test/unit/**/*.spec.js
      --recursive
      
   +---- setup.js

      const chai = require('chai');
      const dirtyChai = require('dirty-chai');
      const chaiChange = require('chai-change');
      const cleanDatabase = require('test/support/cleanDatabase');

      chai.use(dirtyChai);
      chai.use(chaiChange);

      // Comment this line if you're not using a database
      beforeEach(cleanDatabase);
      
   +---- support
   |      +---- cleanDatabase.js

      const container = require('src/container');
      const database = container.resolve('database');

      module.exports = () => database && database.truncate({ cascade: true });
      
   |      +---- factories
   |            +---- user.js

      const dataFaker = require('src/infra/support/dataFaker');

      module.exports = (factory, { User }) => {
        factory.define('user', User, {
          name: dataFaker.name()
        });
      };
      
   |      +---- factory.js

      const path = require('path');
      const { factory, SequelizeAdapter } = require('factory-girl');
      const { FactoriesLoader } = require('src/infra/factoryGirl');
      const models = require('src/infra/database/models');

      const factoryGirl = new factory.FactoryGirl();
      factoryGirl.setAdapter(new SequelizeAdapter());

      module.exports = FactoriesLoader.load({
        factoryGirl,
        models,
        baseFolder: path.join(__dirname, 'factories')
      });
      
   |      +---- request.js

      const request = require('supertest-as-promised');
      const container = require('src/container');
      const server = container.resolve('server');

      module.exports = () => request(server.express);
      
   +---- unit
   |      +---- app
   |            +---- Operation.spec.js

      const { expect } = require('chai');
      const Operation = require('src/app/Operation');

      describe('App :: Operation', () => {
        var CustomOperation;

        beforeEach(() => {
          CustomOperation = class CustomOperation extends Operation {

          };

          CustomOperation.setOutputs(['SUCCESS']);
        });

        describe('#on', () => {
          context('when added handler for a valid output', () => {
            it('does not throw', () => {
              const operation = new CustomOperation();

              expect(() => {
                operation.on(operation.outputs.SUCCESS, () => {});
              }).to.not.throw;
            });
          });

          context('when added handler for a invalid output', () => {
            it('does not throw', () => {
              const operation = new CustomOperation();

              expect(() => {
                operation.on('INVALID', () => {});
              }).to.throw(Error, /Invalid output "INVALID" to operation CustomOperation/);
            });
          });
        });
      });
      
   |            +---- user
   |                  +---- CreateUser.spec.js

      const { expect } = require('chai');
      const CreateUser = require('src/app/user/CreateUser');

      describe('App :: User :: CreateUser', () => {
        var createUser;

        context('when user is valid', () => {
          before(() => {
            const MockUsersRepository = {
              add: (user) => Promise.resolve(user)
            };

            createUser = new CreateUser({
              usersRepository: MockUsersRepository
            });
          });

          it('creates the user and emits SUCCESS', (done) => {
            const userData = { name: 'New User' };

            createUser.on(createUser.outputs.SUCCESS, (response) => {
              expect(response.name).to.equal('New User');
              done();
            });

            createUser.execute(userData);
          });
        });

        context('when user is invalid', () => {
          before(() => {
            const MockUsersRepository = {
              add: () => Promise.reject(Error('ValidationError'))
            };

            createUser = new CreateUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits VALIDATION_ERROR with the error', (done) => {
            const userData = { name: 'New User' };

            createUser.on(createUser.outputs.VALIDATION_ERROR, (response) => {
              expect(response.message).to.equal('ValidationError');
              done();
            });

            createUser.execute(userData);
          });
        });

        context('when there is an internal error', () => {
          before(() => {
            const MockUsersRepository = {
              add: () => Promise.reject(new Error('Some Error'))
            };

            createUser = new CreateUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits ERROR with the error', (done) => {
            const userData = { name: 'New User' };

            createUser.on(createUser.outputs.ERROR, (response) => {
              expect(response.message).to.equal('Some Error');
              done();
            });

            createUser.execute(userData);
          });
        });
      });
      
   |                  +---- DeleteUser.spec.js

      const { expect } = require('chai');
      const DeleteUser = require('src/app/user/DeleteUser');

      describe('App :: User :: DeleteUser', () => {
        var deleteUser;

        context('when user exists', () => {
          before(() => {
            const MockUsersRepository = {
              remove: () => Promise.resolve()
            };

            deleteUser = new DeleteUser({
              usersRepository: MockUsersRepository
            });
          });

          it('deletes the user and emits SUCCESS with no payload', (done) => {
            deleteUser.on(deleteUser.outputs.SUCCESS, (response) => {
              expect(response).to.be.undefined();
              done();
            });

            deleteUser.execute(123);
          });
        });

        context('when the user does not exist', () => {
          before(() => {
            const MockUsersRepository = {
              remove: () => Promise.reject(new Error('NotFoundError'))
            };

            deleteUser = new DeleteUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits NOT_FOUND with the error', (done) => {
            deleteUser.on(deleteUser.outputs.NOT_FOUND, (response) => {
              expect(response.message).to.equal('NotFoundError');
              done();
            });

            deleteUser.execute(123);
          });
        });


        context('when there is an internal error', () => {
          before(() => {
            const MockUsersRepository = {
              remove: () => Promise.reject(new Error('Some Error'))
            };

            deleteUser = new DeleteUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits ERROR with the error', (done) => {
            deleteUser.on(deleteUser.outputs.ERROR, (response) => {
              expect(response.message).to.equal('Some Error');
              done();
            });

            deleteUser.execute(321);
          });
        });
      });
      
   |                  +---- GetAllUsers.spec.js

      const { expect } = require('chai');
      const GetAllUsers = require('src/app/user/GetAllUsers');

      describe('App :: User :: GetAllUsers', () => {
        var getAllUsers;

        context('when query is successful', () => {
          before(() => {
            const MockUsersRepository = {
              getAll: () => Promise.resolve('Imagine all the users...')
            };

            getAllUsers = new GetAllUsers({
              usersRepository: MockUsersRepository
            });
          });

          it('emits SUCCESS with all the users', (done) => {
            getAllUsers.on(getAllUsers.outputs.SUCCESS, (response) => {
              expect(response).to.equal('Imagine all the users...');
              done();
            });

            getAllUsers.execute();
          });
        });

        context('when there is an internal error', () => {
          before(() => {
            const MockUsersRepository = {
              getAll: () => Promise.reject(new Error('Failed'))
            };

            getAllUsers = new GetAllUsers({
              usersRepository: MockUsersRepository
            });
          });

          it('emits ERROR with the error', (done) => {
            getAllUsers.on(getAllUsers.outputs.ERROR, (response) => {
              expect(response.message).to.equal('Failed');

              done();
            });

            getAllUsers.execute();
          });
        });
      });
      
   |                  +---- GetUser.spec.js

      const { expect } = require('chai');
      const GetUser = require('src/app/user/GetUser');

      describe('App :: User :: GetUser', () => {
        let getUser;

        context('when user exists', () => {
          beforeEach(() => {
            const MockUsersRepository = {
              getById: (userId) => Promise.resolve({
                id: userId,
                name: 'The User'
              })
            };

            getUser = new GetUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits SUCCESS with the user', (done) => {
            getUser.on(getUser.outputs.SUCCESS, (user) => {
              expect(user.id).to.equal(123);
              expect(user.name).to.equal('The User');
              done();
            });

            getUser.execute(123);
          });
        });

        context('when user does not exist', () => {
          beforeEach(() => {
            const MockUsersRepository = {
              getById: () => Promise.reject({
                details: 'User with id 123 can\'t be found.'
              })
            };

            getUser = new GetUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits NOT_FOUND with the error', (done) => {
            getUser.on(getUser.outputs.NOT_FOUND, (error) => {
              expect(error.details).to.equal('User with id 123 can\'t be found.');
              done();
            });

            getUser.execute(123);
          });
        });
      });
      
   |                  +---- UpdateUser.spec.js

      const { expect } = require('chai');
      const UpdateUser = require('src/app/user/UpdateUser');

      describe('App :: User :: UpdateUser', () => {
        var updateUser;

        context('when user exists', () => {
          context('when data is valid', () => {
            before(() => {
              const MockUsersRepository = {
                update: (id, data) => Promise.resolve(data)
              };

              updateUser = new UpdateUser({
                usersRepository: MockUsersRepository
              });
            });

            it('updates the user and emits SUCCESS', (done) => {
              const userData = { name: 'Updated User' };

              updateUser.on(updateUser.outputs.SUCCESS, (response) => {
                expect(response.name).to.equal('Updated User');
                done();
              });

              updateUser.execute(123, userData);
            });
          });

          context('when data is invalid', () => {
            before(() => {
              const MockUsersRepository = {
                update: () => Promise.reject(Error('ValidationError'))
              };

              updateUser = new UpdateUser({
                usersRepository: MockUsersRepository
              });
            });

            it('emits VALIDATION_ERROR with the error', (done) => {
              const userData = { name: 'New User' };

              updateUser.on(updateUser.outputs.VALIDATION_ERROR, (response) => {
                expect(response.message).to.equal('ValidationError');
                done();
              });

              updateUser.execute(321, userData);
            });
          });
        });

        context('when the user does not exist', () => {
          before(() => {
            const MockUsersRepository = {
              update: () => Promise.reject(new Error('NotFoundError'))
            };

            updateUser = new UpdateUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits NOT_FOUND with the error', (done) => {
            const userData = { name: 'New User' };

            updateUser.on(updateUser.outputs.NOT_FOUND, (response) => {
              expect(response.message).to.equal('NotFoundError');
              done();
            });

            updateUser.execute(123, userData);
          });
        });


        context('when there is an internal error', () => {
          before(() => {
            const MockUsersRepository = {
              update: () => Promise.reject(new Error('Some Error'))
            };

            updateUser = new UpdateUser({
              usersRepository: MockUsersRepository
            });
          });

          it('emits ERROR with the error', (done) => {
            const userData = { name: 'New User' };

            updateUser.on(updateUser.outputs.ERROR, (response) => {
              expect(response.message).to.equal('Some Error');
              done();
            });

            updateUser.execute(321, userData);
          });
        });
      });
      
   |      +---- domain
   |            +---- user
   |                  +---- User.spec.js

      const { expect } = require('chai');
      const User = require('src/domain/user/User');

      describe('Domain :: User', () => {
        describe('#isLegal', () => {
          context('when user is younger than 21', () => {
            it('returns false', () => {
              const user = new User({ age: 20 });

              expect(user.isLegal()).to.be.false();
            });
          });

          context('when user is 21 years old', () => {
            it('returns true', () => {
              const user = new User({ age: 21 });

              expect(user.isLegal()).to.be.true();
            });
          });
        });
      });
      
   |      +---- infra
   |            +---- logging
   |                  +---- LoggerStreamAdapter.spec.js

      const { expect } = require('chai');
      const LoggerStreamAdapter = require('src/infra/logging/LoggerStreamAdapter');

      describe('Infra :: Logging :: LoggerStreamAdapter', () => {
        describe('.toStream', () => {
          it('wraps the logger into a stream', () => {
            const fakeLogger = {};

            expect(LoggerStreamAdapter.toStream(fakeLogger)).to.have.all.keys('write');
          });

          it('removes the \\n character from the message', () => {
            const fakeLogger = {
              info(message) {
                expect(message).to.equal('Sliced message');
              }
            };

            LoggerStreamAdapter.toStream(fakeLogger).write('Sliced message\n');
          });
        });
      });
      
   |            +---- user
   |                  +---- SequelizeUserMapper.spec.js

      const { expect } = require('chai');
      const User = require('src/domain/user/User');
      const SequelizeUserMapper = require('src/infra/user/SequelizeUserMapper');

      describe('Infra :: User :: SequelizeUserMapper', () => {
        describe('.toEntity', () => {
          it('returns user instance with passed attributes', () => {
            const mockedSequelizeUser = {
              dataValues: {
                id: 1,
                name: 'The Name'
              }
            };

            const entity = SequelizeUserMapper.toEntity(mockedSequelizeUser);

            expect(entity).to.be.instanceOf(User);
            expect(entity.id).to.equal(1);
            expect(entity.name).to.equal('The Name');
          });
        });

        describe('.toDatabase', () => {
          it('returns user object prepared to be persisted', () => {
            const user = new User({
              name: 'Some User'
            });

            const dbUser = SequelizeUserMapper.toDatabase(user);

            expect(dbUser.name).to.equal('Some User');
            expect(dbUser).to.have.all.keys('name');
          });
        });
      });
      
   |                  +---- SequelizeUsersRepository.spec.js

      const { expect } = require('chai');
      const factory = require('test/support/factory');
      const SequelizeUsersRepository = require('src/infra/user/SequelizeUsersRepository');
      const User = require('src/domain/user/User');
      const { User: UserModel } = require('src/infra/database/models');

      describe('Infra :: User :: SequelizeUsersRepository', () => {
        let repository;

        beforeEach(() => {
          repository = new SequelizeUsersRepository({ UserModel });
        });

        describe('#getAll', () => {
          beforeEach(() => {
            return factory.createMany('user', 2, [
              { name: 'User 1' },
              { name: 'User 2' }
            ]);
          });

          it('returns all users from the database', async () => {
            const users = await repository.getAll();

            expect(users).to.have.lengthOf(2);

            expect(users[0]).to.be.instanceOf(User);
            expect(users[0].name).to.equal('User 1');

            expect(users[1]).to.be.instanceOf(User);
            expect(users[1].name).to.equal('User 2');
          });
        });

        describe('#getById', () => {
          context('when user exists', () => {
            it('returns the user', async () => {
              const user = await factory.create('user', {
                name: 'User'
              });

              const foundUser = await repository.getById(user.id);

              expect(foundUser).to.be.instanceOf(User);
              expect(foundUser.id).to.equal(user.id);
              expect(foundUser.name).to.equal('User');
            });
          });

          context('when the user does not exist', () => {
            it('rejects with an error', async () => {
              try {
                await repository.getById(0);
              } catch(error) {
                expect(error.message).to.equal('NotFoundError');
                expect(error.details).to.equal('User with id 0 can\'t be found.');
              }
            });
          });
        });

        describe('#add', () => {
          context('when user is valid', () => {
            it('persists the user', () => {
              const user = new User({
                name: 'The User'
              });

              expect(user.validate().valid).to.be.ok();

              return expect(async () => {
                const persistedUser = await repository.add(user);

                expect(persistedUser.id).to.exist;
                expect(persistedUser.name).to.equal('The User');
              }).to.alter(() => repository.count(), { by: 1 });
            });
          });

          context('when user is invalid', () => {
            it('does not persist the user and rejects with an error', () => {
              const user = new User();

              expect(user.validate().valid).to.not.be.ok();

              return expect(async () => {
                try {
                  await repository.add(user);
                } catch(error) {
                  expect(error.message).to.equal('ValidationError');
                  expect(error.details).to.eql([
                    { message: '"name" is required', path: 'name' }
                  ]);
                }
              }).to.not.alter(() => repository.count());
            });
          });
        });

        describe('#remove', () => {
          context('when the user exists', () => {
            it('removes the user', async () => {
              const user = await factory.create('user', {
                name: 'User'
              });

              return expect(async () => {
                return await repository.remove(user.id);
              }).to.alter(() => repository.count(), { by: -1 });
            });
          });

          context('when the user does not exist', () => {
            it('returns an error', async () => {
              try {
                await repository.remove(0);
              } catch(error) {
                expect(error.message).to.equal('NotFoundError');
                expect(error.details).to.equal('User with id 0 can\'t be found.');
              }
            });
          });
        });

        describe('#update', () => {
          context('when the user exists', () => {
            context('when data is valid', () => {
              it('updates and returns the updated user', async () => {
                const user = await factory.create('user', {
                  name: 'User'
                });

                return expect(async () => {
                  return await repository.update(user.id, { name: 'New User' });
                }).to.alter(async () => {
                  const dbUser = await UserModel.findById(user.id);
                  return dbUser.name;
                }, { from: 'User', to: 'New User' });
              });
            });

            context('when data is not valid', () => {
              it('does not update and returns the error', async () => {
                const user = await factory.create('user', {
                  name: 'User'
                });

                return expect(async () => {
                  try {
                    await repository.update(user.id, { name: '' });
                  } catch(error) {
                    expect(error.message).to.equal('ValidationError');
                  }
                }).to.not.alter(async () => {
                  const dbUser = await UserModel.findById(user.id);
                  return dbUser.name;
                });
              });
            });
          });

          context('when the user does not exist', () => {
            it('returns an error', async () => {
              try {
                await repository.update(0, { name: 'New User' });
              } catch(error) {
                expect(error.message).to.equal('NotFoundError');
                expect(error.details).to.equal('User with id 0 can\'t be found.');
              }
            });
          });
        });
      });
      
   |      +---- interfaces
   |            +---- http
   |                  +---- user
   |                        +---- UserSerializer.spec.js

      const { expect } = require('chai');
      const UserSerializer = require('src/interfaces/http/user/UserSerializer');
      const User = require('src/domain/user/User');

      describe('Interfaces :: HTTP :: User :: UserSerializer', () => {
        it('returns id and name', () => {
          const serializedUser = UserSerializer.serialize({
            id: 123,
            name: 'The User'
          });

          expect(serializedUser).to.eql({
            id: 123,
            name: 'The User'
          });
        });

        it('ignores extra attributes', () => {
          const serializedUser = UserSerializer.serialize({
            id: 321,
            name: 'The User',
            unknown: 'Hello!'
          });

          expect(serializedUser).to.eql({
            id: 321,
            name: 'The User'
          });
        });

        it('is able to serialize user entity instances', () => {
          const user = new User({ id: 1, name: 'User :)' });
          const serializedUser = UserSerializer.serialize(user);

          expect(serializedUser).to.eql({
            id: 1,
            name: 'User :)'
          });
        });
      });
      
   REALWORLD-EXAMPLE-APP
   +---- .circleci
   +---- README.md

    # ![Node/Express/Mongoose Example App](project-logo.png)

    [![Build Status](https://travis-ci.org/anishkny/node-express-realworld-example-app.svg?branch=master)](https://travis-ci.org/anishkny/node-express-realworld-example-app)

    > ### Example Node (Express + Mongoose) codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) API spec.

    <a href="https://thinkster.io/tutorials/node-json-api" target="_blank"><img width="454" src="https://raw.githubusercontent.com/gothinkster/realworld/master/media/learn-btn-hr.png" /></a>

    This repo is functionality complete — PRs and issues welcome!

    # Getting started

    To get the Node server running locally:

    - Clone this repo
    - `npm install` to install all required dependencies
    - Install MongoDB Community Edition ([instructions](https://docs.mongodb.com/manual/installation/#tutorials)) and run it by executing `mongod`
    - `npm run dev` to start the local server

    Alternately, to quickly try out this repo in the cloud, you can [![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/remix/realworld)

    # Code Overview

    ## Dependencies

    - [expressjs](https://github.com/expressjs/express) - The server for handling and routing HTTP requests
    - [express-jwt](https://github.com/auth0/express-jwt) - Middleware for validating JWTs for authentication
    - [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) - For generating JWTs used by authentication
    - [mongoose](https://github.com/Automattic/mongoose) - For modeling and mapping MongoDB data to javascript
    - [mongoose-unique-validator](https://github.com/blakehaswell/mongoose-unique-validator) - For handling unique validation errors in Mongoose. Mongoose only handles validation at the document level, so a unique index across a collection will throw an exception at the driver level. The `mongoose-unique-validator` plugin helps us by formatting the error like a normal mongoose `ValidationError`.
    - [passport](https://github.com/jaredhanson/passport) - For handling user authentication
    - [slug](https://github.com/dodo/node-slug) - For encoding titles into a URL-friendly format

    ## Application Structure

    - `app.js` - The entry point to our application. This file defines our express server and connects it to MongoDB using mongoose. It also requires the routes and models we'll be using in the application.
    - `config/` - This folder contains configuration for passport as well as a central location for configuration/environment variables.
    - `routes/` - This folder contains the route definitions for our API.
    - `models/` - This folder contains the schema definitions for our Mongoose models.

    ## Error Handling

    In `routes/api/index.js`, we define a error-handling middleware for handling Mongoose's `ValidationError`. This middleware will respond with a 422 status code and format the response to have [error messages the clients can understand](https://github.com/gothinkster/realworld/blob/master/API.md#errors-and-status-codes)

    ## Authentication

    Requests are authenticated using the `Authorization` header with a valid JWT. We define two express middlewares in `routes/auth.js` that can be used to authenticate requests. The `required` middleware configures the `express-jwt` middleware using our application's secret and will return a 401 status code if the request cannot be authenticated. The payload of the JWT can then be accessed from `req.payload` in the endpoint. The `optional` middleware configures the `express-jwt` in the same way as `required`, but will *not* return a 401 status code if the request cannot be authenticated.


    <br />

    [![Brought to you by Thinkster](https://raw.githubusercontent.com/gothinkster/realworld/master/media/end.png)](https://thinkster.io)
    
   +---- app.js

    var http = require('http'),
        path = require('path'),
        methods = require('methods'),
        express = require('express'),
        bodyParser = require('body-parser'),
        session = require('express-session'),
        cors = require('cors'),
        passport = require('passport'),
        errorhandler = require('errorhandler'),
        mongoose = require('mongoose');

    var isProduction = process.env.NODE_ENV === 'production';

    // Create global app object
    var app = express();

    app.use(cors());

    // Normal express config defaults
    app.use(require('morgan')('dev'));
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());

    app.use(require('method-override')());
    app.use(express.static(__dirname + '/public'));

    app.use(session({ secret: 'conduit', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false  }));

    if (!isProduction) {
      app.use(errorhandler());
    }

    if(isProduction){
      mongoose.connect(process.env.MONGODB_URI);
    } else {
      mongoose.connect('mongodb://localhost/conduit');
      mongoose.set('debug', true);
    }

    require('./models/User');
    require('./models/Article');
    require('./models/Comment');
    require('./config/passport');

    app.use(require('./routes'));

    /// catch 404 and forward to error handler
    app.use(function(req, res, next) {
      var err = new Error('Not Found');
      err.status = 404;
      next(err);
    });

    /// error handlers

    // development error handler
    // will print stacktrace
    if (!isProduction) {
      app.use(function(err, req, res, next) {
        console.log(err.stack);

        res.status(err.status || 500);

        res.json({'errors': {
          message: err.message,
          error: err
        }});
      });
    }

    // production error handler
    // no stacktraces leaked to user
    app.use(function(err, req, res, next) {
      res.status(err.status || 500);
      res.json({'errors': {
        message: err.message,
        error: {}
      }});
    });

    // finally, let's start our server...
    var server = app.listen( process.env.PORT || 3000, function(){
      console.log('Listening on port ' + server.address().port);
    });
    
   +---- config
   |      +---- index.js

    module.exports = {
      secret: process.env.NODE_ENV === 'production' ? process.env.SECRET : 'secret'
    };
    
   |      +---- passport.js

    var passport = require('passport');
    var LocalStrategy = require('passport-local').Strategy;
    var mongoose = require('mongoose');
    var User = mongoose.model('User');

    passport.use(new LocalStrategy({
      usernameField: 'user[email]',
      passwordField: 'user[password]'
    }, function(email, password, done) {
      User.findOne({email: email}).then(function(user){
        if(!user || !user.validPassword(password)){
          return done(null, false, {errors: {'email or password': 'is invalid'}});
        }

        return done(null, user);
      }).catch(done);
    }));

    
   +---- models
   |      +---- Article.js

    var mongoose = require('mongoose');
    var uniqueValidator = require('mongoose-unique-validator');
    var slug = require('slug');
    var User = mongoose.model('User');

    var ArticleSchema = new mongoose.Schema({
      slug: {type: String, lowercase: true, unique: true},
      title: String,
      description: String,
      body: String,
      favoritesCount: {type: Number, default: 0},
      comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
      tagList: [{ type: String }],
      author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
    }, {timestamps: true});

    ArticleSchema.plugin(uniqueValidator, {message: 'is already taken'});

    ArticleSchema.pre('validate', function(next){
      if(!this.slug)  {
        this.slugify();
      }

      next();
    });

    ArticleSchema.methods.slugify = function() {
      this.slug = slug(this.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
    };

    ArticleSchema.methods.updateFavoriteCount = function() {
      var article = this;

      return User.count({favorites: {$in: [article._id]}}).then(function(count){
        article.favoritesCount = count;

        return article.save();
      });
    };

    ArticleSchema.methods.toJSONFor = function(user){
      return {
        slug: this.slug,
        title: this.title,
        description: this.description,
        body: this.body,
        createdAt: this.createdAt,
        updatedAt: this.updatedAt,
        tagList: this.tagList,
        favorited: user ? user.isFavorite(this._id) : false,
        favoritesCount: this.favoritesCount,
        author: this.author.toProfileJSONFor(user)
      };
    };

    mongoose.model('Article', ArticleSchema);
    
   |      +---- Comment.js

    var mongoose = require('mongoose');

    var CommentSchema = new mongoose.Schema({
      body: String,
      author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
      article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }
    }, {timestamps: true});

    // Requires population of author
    CommentSchema.methods.toJSONFor = function(user){
      return {
        id: this._id,
        body: this.body,
        createdAt: this.createdAt,
        author: this.author.toProfileJSONFor(user)
      };
    };

    mongoose.model('Comment', CommentSchema);
    
   |      +---- User.js

    var mongoose = require('mongoose');
    var uniqueValidator = require('mongoose-unique-validator');
    var crypto = require('crypto');
    var jwt = require('jsonwebtoken');
    var secret = require('../config').secret;

    var UserSchema = new mongoose.Schema({
      username: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/^[a-zA-Z0-9]+$/, 'is invalid'], index: true},
      email: {type: String, lowercase: true, unique: true, required: [true, "can't be blank"], match: [/\S+@\S+\.\S+/, 'is invalid'], index: true},
      bio: String,
      image: String,
      favorites: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Article' }],
      following: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
      hash: String,
      salt: String
    }, {timestamps: true});

    UserSchema.plugin(uniqueValidator, {message: 'is already taken.'});

    UserSchema.methods.validPassword = function(password) {
      var hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
      return this.hash === hash;
    };

    UserSchema.methods.setPassword = function(password){
      this.salt = crypto.randomBytes(16).toString('hex');
      this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
    };

    UserSchema.methods.generateJWT = function() {
      var today = new Date();
      var exp = new Date(today);
      exp.setDate(today.getDate() + 60);

      return jwt.sign({
        id: this._id,
        username: this.username,
        exp: parseInt(exp.getTime() / 1000),
      }, secret);
    };

    UserSchema.methods.toAuthJSON = function(){
      return {
        username: this.username,
        email: this.email,
        token: this.generateJWT(),
        bio: this.bio,
        image: this.image
      };
    };

    UserSchema.methods.toProfileJSONFor = function(user){
      return {
        username: this.username,
        bio: this.bio,
        image: this.image || 'https://static.productionready.io/images/smiley-cyrus.jpg',
        following: user ? user.isFollowing(this._id) : false
      };
    };

    UserSchema.methods.favorite = function(id){
      if(this.favorites.indexOf(id) === -1){
        this.favorites.push(id);
      }

      return this.save();
    };

    UserSchema.methods.unfavorite = function(id){
      this.favorites.remove(id);
      return this.save();
    };

    UserSchema.methods.isFavorite = function(id){
      return this.favorites.some(function(favoriteId){
        return favoriteId.toString() === id.toString();
      });
    };

    UserSchema.methods.follow = function(id){
      if(this.following.indexOf(id) === -1){
        this.following.push(id);
      }

      return this.save();
    };

    UserSchema.methods.unfollow = function(id){
      this.following.remove(id);
      return this.save();
    };

    UserSchema.methods.isFollowing = function(id){
      return this.following.some(function(followId){
        return followId.toString() === id.toString();
      });
    };

    mongoose.model('User', UserSchema);
    
   +---- package.json

    {
      "name": "conduit-node",
      "version": "1.0.0",
      "description": "conduit on node",
      "main": "app.js",
      "scripts": {
        "mongo:start": "docker run --name realworld-mongo -p 27017:27017 mongo & sleep 5",
        "start": "node ./app.js",
        "dev": "nodemon ./app.js",
        "test": "newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json",
        "stop": "lsof -ti :3000 | xargs kill",
        "mongo:stop": "docker stop realworld-mongo && docker rm realworld-mongo"
      },
      "repository": {
        "type": "git",
        "url": "git+https://github.com/gothinkster/productionready-node-api.git"
      },
      "license": "ISC",
      "dependencies": {
        "body-parser": "1.15.0",
        "cors": "2.7.1",
        "ejs": "2.4.1",
        "errorhandler": "1.4.3",
        "express": "4.13.4",
        "express-jwt": "3.3.0",
        "express-session": "1.13.0",
        "jsonwebtoken": "7.1.9",
        "method-override": "2.3.5",
        "methods": "1.1.2",
        "mongoose": "4.4.10",
        "mongoose-unique-validator": "1.0.2",
        "morgan": "1.7.0",
        "passport": "0.3.2",
        "passport-local": "1.0.0",
        "request": "2.69.0",
        "slug": "0.9.1",
        "underscore": "1.8.3"
      },
      "devDependencies": {
        "newman": "^3.8.2",
        "nodemon": "^1.11.0"
      }
    }
    
   +---- public
   +---- routes
   |      +---- api
   |            +---- articles.js

    var router = require('express').Router();
    var mongoose = require('mongoose');
    var Article = mongoose.model('Article');
    var Comment = mongoose.model('Comment');
    var User = mongoose.model('User');
    var auth = require('../auth');

    // Preload article objects on routes with ':article'
    router.param('article', function(req, res, next, slug) {
      Article.findOne({ slug: slug})
        .populate('author')
        .then(function (article) {
          if (!article) { return res.sendStatus(404); }

          req.article = article;

          return next();
        }).catch(next);
    });

    router.param('comment', function(req, res, next, id) {
      Comment.findById(id).then(function(comment){
        if(!comment) { return res.sendStatus(404); }

        req.comment = comment;

        return next();
      }).catch(next);
    });

    router.get('/', auth.optional, function(req, res, next) {
      var query = {};
      var limit = 20;
      var offset = 0;

      if(typeof req.query.limit !== 'undefined'){
        limit = req.query.limit;
      }

      if(typeof req.query.offset !== 'undefined'){
        offset = req.query.offset;
      }

      if( typeof req.query.tag !== 'undefined' ){
        query.tagList = {"$in" : [req.query.tag]};
      }

      Promise.all([
        req.query.author ? User.findOne({username: req.query.author}) : null,
        req.query.favorited ? User.findOne({username: req.query.favorited}) : null
      ]).then(function(results){
        var author = results[0];
        var favoriter = results[1];

        if(author){
          query.author = author._id;
        }

        if(favoriter){
          query._id = {$in: favoriter.favorites};
        } else if(req.query.favorited){
          query._id = {$in: []};
        }

        return Promise.all([
          Article.find(query)
            .limit(Number(limit))
            .skip(Number(offset))
            .sort({createdAt: 'desc'})
            .populate('author')
            .exec(),
          Article.count(query).exec(),
          req.payload ? User.findById(req.payload.id) : null,
        ]).then(function(results){
          var articles = results[0];
          var articlesCount = results[1];
          var user = results[2];

          return res.json({
            articles: articles.map(function(article){
              return article.toJSONFor(user);
            }),
            articlesCount: articlesCount
          });
        });
      }).catch(next);
    });

    router.get('/feed', auth.required, function(req, res, next) {
      var limit = 20;
      var offset = 0;

      if(typeof req.query.limit !== 'undefined'){
        limit = req.query.limit;
      }

      if(typeof req.query.offset !== 'undefined'){
        offset = req.query.offset;
      }

      User.findById(req.payload.id).then(function(user){
        if (!user) { return res.sendStatus(401); }

        Promise.all([
          Article.find({ author: {$in: user.following}})
            .limit(Number(limit))
            .skip(Number(offset))
            .populate('author')
            .exec(),
          Article.count({ author: {$in: user.following}})
        ]).then(function(results){
          var articles = results[0];
          var articlesCount = results[1];

          return res.json({
            articles: articles.map(function(article){
              return article.toJSONFor(user);
            }),
            articlesCount: articlesCount
          });
        }).catch(next);
      });
    });

    router.post('/', auth.required, function(req, res, next) {
      User.findById(req.payload.id).then(function(user){
        if (!user) { return res.sendStatus(401); }

        var article = new Article(req.body.article);

        article.author = user;

        return article.save().then(function(){
          console.log(article.author);
          return res.json({article: article.toJSONFor(user)});
        });
      }).catch(next);
    });

    // return a article
    router.get('/:article', auth.optional, function(req, res, next) {
      Promise.all([
        req.payload ? User.findById(req.payload.id) : null,
        req.article.populate('author').execPopulate()
      ]).then(function(results){
        var user = results[0];

        return res.json({article: req.article.toJSONFor(user)});
      }).catch(next);
    });

    // update article
    router.put('/:article', auth.required, function(req, res, next) {
      User.findById(req.payload.id).then(function(user){
        if(req.article.author._id.toString() === req.payload.id.toString()){
          if(typeof req.body.article.title !== 'undefined'){
            req.article.title = req.body.article.title;
          }

          if(typeof req.body.article.description !== 'undefined'){
            req.article.description = req.body.article.description;
          }

          if(typeof req.body.article.body !== 'undefined'){
            req.article.body = req.body.article.body;
          }

          if(typeof req.body.article.tagList !== 'undefined'){
            req.article.tagList = req.body.article.tagList
          }

          req.article.save().then(function(article){
            return res.json({article: article.toJSONFor(user)});
          }).catch(next);
        } else {
          return res.sendStatus(403);
        }
      });
    });

    // delete article
    router.delete('/:article', auth.required, function(req, res, next) {
      User.findById(req.payload.id).then(function(user){
        if (!user) { return res.sendStatus(401); }

        if(req.article.author._id.toString() === req.payload.id.toString()){
          return req.article.remove().then(function(){
            return res.sendStatus(204);
          });
        } else {
          return res.sendStatus(403);
        }
      }).catch(next);
    });

    // Favorite an article
    router.post('/:article/favorite', auth.required, function(req, res, next) {
      var articleId = req.article._id;

      User.findById(req.payload.id).then(function(user){
        if (!user) { return res.sendStatus(401); }

        return user.favorite(articleId).then(function(){
          return req.article.updateFavoriteCount().then(function(article){
            return res.json({article: article.toJSONFor(user)});
          });
        });
      }).catch(next);
    });

    // Unfavorite an article
    router.delete('/:article/favorite', auth.required, function(req, res, next) {
      var articleId = req.article._id;

      User.findById(req.payload.id).then(function (user){
        if (!user) { return res.sendStatus(401); }

        return user.unfavorite(articleId).then(function(){
          return req.article.updateFavoriteCount().then(function(article){
            return res.json({article: article.toJSONFor(user)});
          });
        });
      }).catch(next);
    });

    // return an article's comments
    router.get('/:article/comments', auth.optional, function(req, res, next){
      Promise.resolve(req.payload ? User.findById(req.payload.id) : null).then(function(user){
        return req.article.populate({
          path: 'comments',
          populate: {
            path: 'author'
          },
          options: {
            sort: {
              createdAt: 'desc'
            }
          }
        }).execPopulate().then(function(article) {
          return res.json({comments: req.article.comments.map(function(comment){
            return comment.toJSONFor(user);
          })});
        });
      }).catch(next);
    });

    // create a new comment
    router.post('/:article/comments', auth.required, function(req, res, next) {
      User.findById(req.payload.id).then(function(user){
        if(!user){ return res.sendStatus(401); }

        var comment = new Comment(req.body.comment);
        comment.article = req.article;
        comment.author = user;

        return comment.save().then(function(){
          req.article.comments.push(comment);

          return req.article.save().then(function(article) {
            res.json({comment: comment.toJSONFor(user)});
          });
        });
      }).catch(next);
    });

    router.delete('/:article/comments/:comment', auth.required, function(req, res, next) {
      if(req.comment.author.toString() === req.payload.id.toString()){
        req.article.comments.remove(req.comment._id);
        req.article.save()
          .then(Comment.find({_id: req.comment._id}).remove().exec())
          .then(function(){
            res.sendStatus(204);
          });
      } else {
        res.sendStatus(403);
      }
    });

    module.exports = router;
    
   |            +---- index.js

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

    router.use('/', require('./users'));
    router.use('/profiles', require('./profiles'));
    router.use('/articles', require('./articles'));
    router.use('/tags', require('./tags'));

    router.use(function(err, req, res, next){
      if(err.name === 'ValidationError'){
        return res.status(422).json({
          errors: Object.keys(err.errors).reduce(function(errors, key){
            errors[key] = err.errors[key].message;

            return errors;
          }, {})
        });
      }

      return next(err);
    });

    module.exports = router;
   |            +---- profiles.js

    var router = require('express').Router();
    var mongoose = require('mongoose');
    var User = mongoose.model('User');
    var auth = require('../auth');

    // Preload user profile on routes with ':username'
    router.param('username', function(req, res, next, username){
      User.findOne({username: username}).then(function(user){
        if (!user) { return res.sendStatus(404); }

        req.profile = user;

        return next();
      }).catch(next);
    });

    router.get('/:username', auth.optional, function(req, res, next){
      if(req.payload){
        User.findById(req.payload.id).then(function(user){
          if(!user){ return res.json({profile: req.profile.toProfileJSONFor(false)}); }

          return res.json({profile: req.profile.toProfileJSONFor(user)});
        });
      } else {
        return res.json({profile: req.profile.toProfileJSONFor(false)});
      }
    });

    router.post('/:username/follow', auth.required, function(req, res, next){
      var profileId = req.profile._id;

      User.findById(req.payload.id).then(function(user){
        if (!user) { return res.sendStatus(401); }

        return user.follow(profileId).then(function(){
          return res.json({profile: req.profile.toProfileJSONFor(user)});
        });
      }).catch(next);
    });

    router.delete('/:username/follow', auth.required, function(req, res, next){
      var profileId = req.profile._id;

      User.findById(req.payload.id).then(function(user){
        if (!user) { return res.sendStatus(401); }

        return user.unfollow(profileId).then(function(){
          return res.json({profile: req.profile.toProfileJSONFor(user)});
        });
      }).catch(next);
    });

    module.exports = router;
    
   |            +---- tags.js

    var router = require('express').Router();
    var mongoose = require('mongoose');
    var Article = mongoose.model('Article');

    // return a list of tags
    router.get('/', function(req, res, next) {
      Article.find().distinct('tagList').then(function(tags){
        return res.json({tags: tags});
      }).catch(next);
    });

    module.exports = router;
    
   |            +---- users.js

    var mongoose = require('mongoose');
    var router = require('express').Router();
    var passport = require('passport');
    var User = mongoose.model('User');
    var auth = require('../auth');

    router.get('/user', auth.required, function(req, res, next){
      User.findById(req.payload.id).then(function(user){
        if(!user){ return res.sendStatus(401); }

        return res.json({user: user.toAuthJSON()});
      }).catch(next);
    });

    router.put('/user', auth.required, function(req, res, next){
      User.findById(req.payload.id).then(function(user){
        if(!user){ return res.sendStatus(401); }

        // only update fields that were actually passed...
        if(typeof req.body.user.username !== 'undefined'){
          user.username = req.body.user.username;
        }
        if(typeof req.body.user.email !== 'undefined'){
          user.email = req.body.user.email;
        }
        if(typeof req.body.user.bio !== 'undefined'){
          user.bio = req.body.user.bio;
        }
        if(typeof req.body.user.image !== 'undefined'){
          user.image = req.body.user.image;
        }
        if(typeof req.body.user.password !== 'undefined'){
          user.setPassword(req.body.user.password);
        }

        return user.save().then(function(){
          return res.json({user: user.toAuthJSON()});
        });
      }).catch(next);
    });

    router.post('/users/login', function(req, res, next){
      if(!req.body.user.email){
        return res.status(422).json({errors: {email: "can't be blank"}});
      }

      if(!req.body.user.password){
        return res.status(422).json({errors: {password: "can't be blank"}});
      }

      passport.authenticate('local', {session: false}, function(err, user, info){
        if(err){ return next(err); }

        if(user){
          user.token = user.generateJWT();
          return res.json({user: user.toAuthJSON()});
        } else {
          return res.status(422).json(info);
        }
      })(req, res, next);
    });

    router.post('/users', function(req, res, next){
      var user = new User();

      user.username = req.body.user.username;
      user.email = req.body.user.email;
      user.setPassword(req.body.user.password);

      user.save().then(function(){
        return res.json({user: user.toAuthJSON()});
      }).catch(next);
    });

    module.exports = router;
    
   |      +---- auth.js

    var jwt = require('express-jwt');
    var secret = require('../config').secret;

    function getTokenFromHeader(req){
      if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token' ||
          req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
        return req.headers.authorization.split(' ')[1];
      }

      return null;
    }

    var auth = {
      required: jwt({
        secret: secret,
        userProperty: 'payload',
        getToken: getTokenFromHeader
      }),
      optional: jwt({
        secret: secret,
        userProperty: 'payload',
        credentialsRequired: false,
        getToken: getTokenFromHeader
      })
    };

    module.exports = auth;
    
   |      +---- index.js

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

    router.use('/api', require('./api'));

    module.exports = router;
    
   +---- tests
   |      +---- api-tests.postman.json

    {
      "variables": [],
      "info": {
        "name": "Conduit API Tests",
        "_postman_id": "dda3e595-02d7-bf12-2a43-3daea0970192",
        "description": "Collection for testing the Conduit API\n\nhttps://github.com/gothinkster/realworld",
        "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
      },
      "item": [{
          "name": "Auth",
          "description": "",
          "item": [{
              "name": "Register",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "if (!(environment.isIntegrationTest)) {",
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');",
                    "",
                    "var user = responseJSON.user || {};",
                    "",
                    "tests['User has \"email\" property'] = user.hasOwnProperty('email');",
                    "tests['User has \"username\" property'] = user.hasOwnProperty('username');",
                    "tests['User has \"token\" property'] = user.hasOwnProperty('token');",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/users",
                "method": "POST",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\", \"username\":\"johnjacob\"}}"
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Login",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');",
                    "",
                    "var user = responseJSON.user || {};",
                    "",
                    "tests['User has \"email\" property'] = user.hasOwnProperty('email');",
                    "tests['User has \"username\" property'] = user.hasOwnProperty('username');",
                    "tests['User has \"token\" property'] = user.hasOwnProperty('token');",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/users/login",
                "method": "POST",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\"}}"
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Login and Remember Token",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');",
                    "",
                    "var user = responseJSON.user || {};",
                    "",
                    "tests['User has \"email\" property'] = user.hasOwnProperty('email');",
                    "tests['User has \"username\" property'] = user.hasOwnProperty('username');",
                    "tests['User has \"token\" property'] = user.hasOwnProperty('token');",
                    "",
                    "if(tests['User has \"token\" property']){",
                    "    postman.setEnvironmentVariable('token', user.token);",
                    "}",
                    "",
                    "tests['Environment variable \"token\" has been set'] = environment.token === user.token;",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/users/login",
                "method": "POST",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\"}}"
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Current User",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');",
                    "",
                    "var user = responseJSON.user || {};",
                    "",
                    "tests['User has \"email\" property'] = user.hasOwnProperty('email');",
                    "tests['User has \"username\" property'] = user.hasOwnProperty('username');",
                    "tests['User has \"token\" property'] = user.hasOwnProperty('token');",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/user",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            },
            {
              "name": "Update User",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');",
                    "",
                    "var user = responseJSON.user || {};",
                    "",
                    "tests['User has \"email\" property'] = user.hasOwnProperty('email');",
                    "tests['User has \"username\" property'] = user.hasOwnProperty('username');",
                    "tests['User has \"token\" property'] = user.hasOwnProperty('token');",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/user",
                "method": "PUT",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"user\":{\"email\":\"john@jacob.com\"}}"
                },
                "description": ""
              },
              "response": []
            }
          ]
        },
        {
          "name": "Articles with authentication",
          "description": "",
          "item": [{
              "name": "Feed",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/feed",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "All Articles",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "All Articles with auth",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles by Author",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?author=johnjacob",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "author",
                    "value": "johnjacob"
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles by Author with auth",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?author=johnjacob",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "author",
                    "value": "johnjacob",
                    "equals": true,
                    "description": ""
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles Favorited by Username",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "    ",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?favorited=jane",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "favorited",
                    "value": "jane"
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles Favorited by Username with auth",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "    ",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?favorited=jane",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "favorited",
                    "value": "jane"
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles by Tag",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?tag=dragons",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "tag",
                    "value": "dragons"
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Create Article",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');",
                    "",
                    "var article = responseJSON.article || {};",
                    "",
                    "tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "if(tests['Article has \"slug\" property']){",
                    "    postman.setEnvironmentVariable('slug', article.slug);",
                    "}",
                    "tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles",
                "method": "POST",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"article\":{\"title\":\"How to train your dragon\", \"description\":\"Ever wonder how?\", \"body\":\"Very carefully.\", \"tagList\":[\"dragons\",\"training\"]}}"
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Single Article by slug",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');",
                    "",
                    "var article = responseJSON.article || {};",
                    "",
                    "tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            },
            {
              "name": "Update Article",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "if (!(environment.isIntegrationTest)) {",
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');",
                    "",
                    "var article = responseJSON.article || {};",
                    "",
                    "tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}",
                "method": "PUT",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"article\":{\"body\":\"With two hands\"}}"
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Favorite Article",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');",
                    "",
                    "var article = responseJSON.article || {};",
                    "",
                    "tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "tests[\"Article's 'favorited' property is true\"] = article.favorited === true;",
                    "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "tests[\"Article's 'favoritesCount' property is greater than 0\"] = article.favoritesCount > 0;",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}/favorite",
                "method": "POST",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Unfavorite Article",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');",
                    "",
                    "var article = responseJSON.article || {};",
                    "",
                    "tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "tests[\"Article's \\\"favorited\\\" property is true\"] = article.favorited === false;",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}/favorite",
                "method": "DELETE",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            }
          ]
        },
        {
          "name": "Articles",
          "description": "",
          "item": [{
              "name": "All Articles",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles by Author",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?author=johnjacob",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "author",
                    "value": "johnjacob"
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles Favorited by Username",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "    ",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?favorited=jane",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "favorited",
                    "value": "jane"
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Articles by Tag",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');",
                    "    tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');",
                    "    tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);",
                    "",
                    "    if(responseJSON.articles.length){",
                    "        var article = responseJSON.articles[0];",
                    "",
                    "        tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "        tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "        tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "        tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "        tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "        tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "        tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "        tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "        tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "        tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "        tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "        tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "        tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "        tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    "    } else {",
                    "        tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": {
                  "raw": "{{apiUrl}}/articles?tag=dragons",
                  "host": [
                    "{{apiUrl}}"
                  ],
                  "path": [
                    "articles"
                  ],
                  "query": [{
                    "key": "tag",
                    "value": "dragons"
                  }],
                  "variable": []
                },
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": ""
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Single Article by slug",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');",
                    "",
                    "var article = responseJSON.article || {};",
                    "",
                    "tests['Article has \"title\" property'] = article.hasOwnProperty('title');",
                    "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');",
                    "tests['Article has \"body\" property'] = article.hasOwnProperty('body');",
                    "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');",
                    "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(article.createdAt).toISOString() === article.createdAt;",
                    "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');",
                    "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(article.updatedAt).toISOString() === article.updatedAt;",
                    "tests['Article has \"description\" property'] = article.hasOwnProperty('description');",
                    "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');",
                    "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);",
                    "tests['Article has \"author\" property'] = article.hasOwnProperty('author');",
                    "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');",
                    "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');",
                    "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            }
          ]
        },
        {
          "name": "Comments",
          "description": "",
          "item": [{
              "name": "All Comments for Article",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var is200Response = responseCode.code === 200",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"comments\" property'] = responseJSON.hasOwnProperty('comments');",
                    "",
                    "    if(responseJSON.comments.length){",
                    "        var comment = responseJSON.comments[0];",
                    "",
                    "        tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');",
                    "        tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');",
                    "        tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');",
                    "        tests['\"createdAt\" property is an ISO 8601 timestamp'] = new Date(comment.createdAt).toISOString() === comment.createdAt;",
                    "        tests['Comment has \"updatedAt\" property'] = comment.hasOwnProperty('updatedAt');",
                    "        tests['\"updatedAt\" property is an ISO 8601 timestamp'] = new Date(comment.updatedAt).toISOString() === comment.updatedAt;",
                    "        tests['Comment has \"author\" property'] = comment.hasOwnProperty('author');",
                    "    }",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}/comments",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            },
            {
              "name": "Create Comment for Article",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "var responseJSON = JSON.parse(responseBody);",
                    "",
                    "tests['Response contains \"comment\" property'] = responseJSON.hasOwnProperty('comment');",
                    "",
                    "var comment = responseJSON.comment || {};",
                    "",
                    "tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');",
                    "tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');",
                    "tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');",
                    "tests['\"createdAt\" property is an ISO 8601 timestamp'] = new Date(comment.createdAt).toISOString() === comment.createdAt;",
                    "tests['Comment has \"author\" property'] = comment.hasOwnProperty('author');",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}/comments",
                "method": "POST",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"comment\":{\"body\":\"Thank you so much!\"}}"
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Delete Comment for Article",
              "request": {
                "url": "{{apiUrl}}/articles/{{slug}}/comments/1",
                "method": "DELETE",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            }
          ]
        },
        {
          "name": "Profiles",
          "description": "",
          "item": [{
              "name": "Profile",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "if (!(environment.isIntegrationTest)) {",
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');",
                    "    ",
                    "    var profile = responseJSON.profile || {};",
                    "    ",
                    "    tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');",
                    "    tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');",
                    "    tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');",
                    "}",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/profiles/johnjacob",
                "method": "GET",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            },
            {
              "name": "Follow Profile",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "if (!(environment.isIntegrationTest)) {",
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');",
                    "    ",
                    "    var profile = responseJSON.profile || {};",
                    "    ",
                    "    tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');",
                    "    tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');",
                    "    tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');",
                    "    tests['Profile\\'s \"following\" property is true'] = profile.following === true;",
                    "}",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/profiles/johnjacob/follow",
                "method": "POST",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\"user\":{\"email\":\"john@jacob.com\"}}"
                },
                "description": ""
              },
              "response": []
            },
            {
              "name": "Unfollow Profile",
              "event": [{
                "listen": "test",
                "script": {
                  "type": "text/javascript",
                  "exec": [
                    "if (!(environment.isIntegrationTest)) {",
                    "var is200Response = responseCode.code === 200;",
                    "",
                    "tests['Response code is 200 OK'] = is200Response;",
                    "",
                    "if(is200Response){",
                    "    var responseJSON = JSON.parse(responseBody);",
                    "",
                    "    tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');",
                    "    ",
                    "    var profile = responseJSON.profile || {};",
                    "    ",
                    "    tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');",
                    "    tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');",
                    "    tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');",
                    "    tests['Profile\\'s \"following\" property is false'] = profile.following === false;",
                    "}",
                    "}",
                    ""
                  ]
                }
              }],
              "request": {
                "url": "{{apiUrl}}/profiles/johnjacob/follow",
                "method": "DELETE",
                "header": [{
                    "key": "Content-Type",
                    "value": "application/json",
                    "description": ""
                  },
                  {
                    "key": "X-Requested-With",
                    "value": "XMLHttpRequest",
                    "description": ""
                  },
                  {
                    "key": "Authorization",
                    "value": "Token {{token}}",
                    "description": ""
                  }
                ],
                "body": {},
                "description": ""
              },
              "response": []
            }
          ]
        },
        {
          "name": "Tags",
          "description": "",
          "item": [{
            "name": "All Tags",
            "event": [{
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "var is200Response = responseCode.code === 200;",
                  "",
                  "tests['Response code is 200 OK'] = is200Response;",
                  "",
                  "if(is200Response){",
                  "    var responseJSON = JSON.parse(responseBody);",
                  "    ",
                  "    tests['Response contains \"tags\" property'] = responseJSON.hasOwnProperty('tags');",
                  "    tests['\"tags\" property returned as array'] = Array.isArray(responseJSON.tags);",
                  "}",
                  ""
                ]
              }
            }],
            "request": {
              "url": "{{apiUrl}}/tags",
              "method": "GET",
              "header": [{
                  "key": "Content-Type",
                  "value": "application/json",
                  "description": ""
                },
                {
                  "key": "X-Requested-With",
                  "value": "XMLHttpRequest",
                  "description": ""
                }
              ],
              "body": {
                "mode": "raw",
                "raw": ""
              },
              "description": ""
            },
            "response": []
          }]
        },
        {
          "name": "Cleanup",
          "description": "",
          "item": [{
            "name": "Delete Article",
            "request": {
              "url": "{{apiUrl}}/articles/{{slug}}",
              "method": "DELETE",
              "header": [{
                  "key": "Content-Type",
                  "value": "application/json",
                  "description": ""
                },
                {
                  "key": "X-Requested-With",
                  "value": "XMLHttpRequest",
                  "description": ""
                },
                {
                  "key": "Authorization",
                  "value": "Token {{token}}",
                  "description": ""
                }
              ],
              "body": {
                "mode": "raw",
                "raw": ""
              },
              "description": ""
            },
            "response": []
          }]
        }
      ]
    }
    
   |      +---- env-api-tests.postman.json

    {
      "id": "4aa60b52-97fc-456d-4d4f-14a350e95dff",
      "name": "Conduit API Tests - Environment",
      "values": [{
        "enabled": true,
        "key": "apiUrl",
        "value": "http://localhost:3000/api",
        "type": "text"
      }],
      "timestamp": 1505871382668,
      "_postman_variable_scope": "environment",
      "_postman_exported_at": "2017-09-20T01:36:34.835Z",
      "_postman_exported_using": "Postman/5.2.0"
    }
    
   API, ARCHITECTURE (+TypeScript)

    src
    │   app.js          # App entry point
    └───api             # Express route controllers for all the endpoints of the app
    └───config          # Environment variables and configuration related stuff
    └───jobs            # Jobs definitions for agenda.js
    └───loaders         # Split the startup process into modules
    └───models          # Database models
    └───services        # All the business logic is here
    └───subscribers     # Event handlers for async task
    └───types           # Type declaration files (d.ts) for Typescript
    
   +---- .circleci
   |      +---- config.yml

    version: 2
    jobs:
      build:
        docker:
          - image: circleci/node:10.15
          - image: circleci/mongo:latest
        steps:
          - checkout
          - run:
              name: install-npm
              command: npm install
          - save_cache:
              key: dependency-cache-{{ checksum "package.json" }}
              paths:
                - ./node_modules
          - run:
              name: test
              command: npm test
   +---- .env

    # JSON web token (JWT) secret: this keeps our app's user authentication secure
    # This secret should be a random 20-ish character string
    JWT_SECRET ='p4sta.w1th-b0logn3s3-s@uce'

    # Mongo DB
    # Local development
    MONGODB_URI='mongodb://localhost/bulletproof-nodejs'

    # Port
    PORT=3000

    # Debug
    LOG_LEVEL='debug'
    
   +---- .eslintrc.js

    module.exports =  {
      parser:  '@typescript-eslint/parser',  // Specifies the ESLint parser
      extends:  [
        'plugin:@typescript-eslint/recommended',  // Uses the recommended rules from the @typescript-eslint/eslint-plugin
        'prettier/@typescript-eslint',  // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
        'plugin:prettier/recommended',  // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
      ],
      parserOptions:  {
        ecmaVersion:  2018,  // Allows for the parsing of modern ECMAScript features
        sourceType:  'module',  // Allows for the use of imports
      },
      rules:  {
        '@typescript-eslint/explicit-member-accessibility': 0,
        '@typescript-eslint/explicit-function-return-type': 0,
        '@typescript-eslint/no-parameter-properties': 0,
        '@typescript-eslint/interface-name-prefix': 0
      },
    };
    
   +---- .gitignore

    # See https://help.github.com/ignore-files/ for more about ignoring files.
    # dependencies
    /node_modules

    # misc
    .DS_Store
    .env*

    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    .env
    build
   +---- .prettierrc.js

    module.exports =  {
      semi:  true,
      trailingComma:  'all',
      singleQuote:  true,
      printWidth:  120,
      tabWidth:  2,
    };
    
   +---- README.md

    # Bulletproof Node.js architecture 🛡️

    This is the example repository from the blog post ['Bulletproof node.js project architecture'](https://softwareontheroad.com/ideal-nodejs-project-structure?utm_source=github&utm_medium=readme)

    Please read the blog post in order to have a good understanding of the server architecture.

    Also, I added lots of comments to the code that are not in the blog post, because they explain the implementation and the reason behind the choices of libraries and some personal opinions and some bad jokes.

    The API by itself doesn't do anything fancy, it's just a user CRUD with authentication capabilities.
    Maybe we can transform this into something useful, a more advanced example, just open an issue and let's discuss the future of the repo.

    ## Development

    We use `node` version `10.15.0`

    ```
    nvm install 10.15.0
    ```

    ```
    nvm use 10.15.0
    ```

    The first time, you will need to run

    ```
    npm install
    ```

    Then just start the server with

    ```
    npm run start
    ```
    It uses nodemon for livereloading :peace-fingers:

    # API Validation

    By using celebrate the req.body schema becomes clary defined at route level, so even frontend devs can read what an API endpoint expects without need to writting a documentation that can get outdated quickly.

    ```js
    route.post('/signup',
      celebrate({
        body: Joi.object({
          name: Joi.string().required(),
          email: Joi.string().required(),
          password: Joi.string().required(),
        }),
      }),
      controller.signup)
    ```

    **Example error**

    ```json
    {
      "errors": {
        "message": "child \"email\" fails because [\"email\" is required]"
      }
    }
    ```

    [Read more about celebrate here](https://github.com/arb/celebrate) and [the Joi validation API](https://github.com/hapijs/joi/blob/v15.0.1/API.md)

    # Roadmap
    - [x] API Validation layer (Celebrate+Joi)
    - [ ] Unit tests examples
    - [ ] [Cluster mode](https://softwareontheroad.com/nodejs-scalability-issues?utm_source=github&utm_medium=readme)
    - [x] The logging _'layer'_
    - [ ] Add ageda dashboard
    - [x] Continuous integration with CircleCI 😍
    - [ ] Deploys script and docs for AWS Elastic Beanstalk and Heroku
    - [ ] Integration test with newman 😉
    - [ ] Instructions on typescript debugging with VSCode


    # FAQ

    ## Where should I put the FrontEnd code? Is this a good backend for Angular or React or Vue or _whatever_ ?

      [It's not a good idea to have node.js serving static assets a.k.a the frontend](https://softwareontheroad.com/nodejs-scalability-issues?utm_source=github&utm_medium=readme)

      Also, I don't wanna take part in frontend frameworks wars 😅

      Just use the frontend framework you like the most _or hate the less_ it will work 😁

    ## Don't you think you can add X layer to do Y? Why do you still use express if the Serverless Framework is better and it's more reliable?

      I know this is not a perfect architecture but it's the most scalable that I know with less code and headache that I know.

      It's meant for small startups or one-developer army projects.

      I know if you start moving layers into another technology, you will end up with your business/domain logic into npm packages, your routing layer will be pure AWS Lambda functions and your data layer a combination of DynamoDB, Redis, maybe redshift, and Agolia.

      Take a deep breath and go slowly, let the business grow and then scale up your product. You will need a team and talented developers anyway.
    
   +---- jest.config.js

    // For a detailed explanation regarding each configuration property, visit:
    // https://jestjs.io/docs/en/configuration.html

    module.exports = {
      // All imported modules in your tests should be mocked automatically
      // automock: false,

      // Stop running tests after the first failure
      // bail: false,

      // Respect "browser" field in package.json when resolving modules
      // browser: false,

      // The directory where Jest should store its cached dependency information
      // cacheDirectory: "/var/folders/bw/vvybgj3d3kgb98nzjxfmpv5c0000gn/T/jest_dx",

      // Automatically clear mock calls and instances between every test
      // clearMocks: false,

      // Indicates whether the coverage information should be collected while executing the test
      // collectCoverage: false,

      // An array of glob patterns indicating a set of files for which coverage information should be collected
      // collectCoverageFrom: null,

      // The directory where Jest should output its coverage files
      // coverageDirectory: null,

      // An array of regexp pattern strings used to skip coverage collection
      // coveragePathIgnorePatterns: [
      //   "/node_modules/"
      // ],

      // A list of reporter names that Jest uses when writing coverage reports
      // coverageReporters: [
      //   "json",
      //   "text",
      //   "lcov",
      //   "clover"
      // ],

      // An object that configures minimum threshold enforcement for coverage results
      // coverageThreshold: null,

      // Make calling deprecated APIs throw helpful error messages
      // errorOnDeprecated: false,

      // Force coverage collection from ignored files usin a array of glob patterns
      // forceCoverageMatch: [],

      // A path to a module which exports an async function that is triggered once before all test suites
      // globalSetup: null,

      // A path to a module which exports an async function that is triggered once after all test suites
      // globalTeardown: null,

      // A set of global variables that need to be available in all test environments
      // globals: {},

      // An array of directory names to be searched recursively up from the requiring module's location
      // moduleDirectories: [
      //   "node_modules"
      // ],

      // An array of file extensions your modules use
      moduleFileExtensions: [
        'js',
        'ts',
        'json'
      ],

      // A map from regular expressions to module names that allow to stub out resources with a single module
      // moduleNameMapper: {},

      // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
      // modulePathIgnorePatterns: [],

      // Activates notifications for test results
      // notify: false,

      // An enum that specifies notification mode. Requires { notify: true }
      // notifyMode: "always",

      // A preset that is used as a base for Jest's configuration
      preset: 'ts-jest',

      // Run tests from one or more projects
      // projects: null,

      // Use this configuration option to add custom reporters to Jest
      // reporters: undefined,

      // Automatically reset mock state between every test
      // resetMocks: false,

      // Reset the module registry before running each individual test
      // resetModules: false,

      // A path to a custom resolver
      // resolver: null,

      // Automatically restore mock state between every test
      // restoreMocks: false,

      // The root directory that Jest should scan for tests and modules within
      // rootDir: null,

      // A list of paths to directories that Jest should use to search for files in
      // roots: [
      //   "<rootDir>"
      // ],

      // Allows you to use a custom runner instead of Jest's default test runner
      // runner: "jest-runner",

      // The paths to modules that run some code to configure or set up the testing environment before each test
      // setupFiles: [],

      // The path to a module that runs some code to configure or set up the testing framework before each test
      // setupTestFrameworkScriptFile: './tests/setup.js',

      // A list of paths to snapshot serializer modules Jest should use for snapshot testing
      // snapshotSerializers: [],

      // The test environment that will be used for testing
      testEnvironment: 'node',

      // Options that will be passed to the testEnvironment
      // testEnvironmentOptions: {},

      // Adds a location field to test results
      // testLocationInResults: false,

      // The glob patterns Jest uses to detect test files
      testMatch: [
        // '**/?(*.)+(spec|test).js?(x)',
        '**/?(*.)+(spec|test).ts?(x)',
      ],

      // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
      // testPathIgnorePatterns: [
      //   "/node_modules/"
      // ],

      // The regexp pattern Jest uses to detect test files
      // testRegex: "",

      // This option allows the use of a custom results processor
      // testResultsProcessor: null,

      // This option allows use of a custom test runner
      // testRunner: "jasmine2",

      // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
      // testURL: "http://localhost",

      // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
      // timers: "real",

      // A map from regular expressions to paths to transformers
      transform: {
        '^.+\\.ts?$': 'ts-jest',
      },

      // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
      // transformIgnorePatterns: [
      //   "/node_modules/"
      // ],

      // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
      // unmockedModulePathPatterns: undefined,

      // Indicates whether each individual test should be reported during the run
      // verbose: null,

      // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
      // watchPathIgnorePatterns: [],

      // Whether to use watchman for file crawling
      // watchman: true,
    };
    
   +---- nodemon.json

    {
      "watch": [
        "src",
        ".env"
      ],
      "ext": "js,ts,json",
      "ignore": [
        "src/**/*.spec.ts"
      ],
      "exec": "ts-node  --transpile-only ./src/app.ts"
    }
   +---- package.json

    {
      "name": "bulletproof-nodejs",
      "version": "1.0.0",
      "description": "Bulletproof node.js",
      "main": "src/app.ts",
      "scripts": {
        "build": "tsc",
        "heroku-postbuild": "npm run build",
        "start": "nodemon",
        "inspect": "nodemon --inspect src/app.ts",
        "test": "jest",
        "lint": "npm run lint:js ",
        "lint:eslint": "eslint --ignore-path .gitignore --ext .ts",
        "lint:js": "npm run lint:eslint src/",
        "lint:fix": "npm run lint:js -- --fix"
      },
      "repository": {
        "type": "git",
        "url": "git+https://github.com/santiq/bulletproof-nodejs.git"
      },
      "keywords": [
        "boilerplay",
        "cron",
        "jobs",
        "js",
        "javascript",
        "typescript",
        "node",
        "express"
      ],
      "author": "Santiago Quinteros",
      "license": "ISC",
      "dependencies": {
        "agenda": "^2.0.2",
        "agendash": "^1.0.0",
        "argon2": "^0.21.0",
        "body-parser": "^1.18.2",
        "celebrate": "^9.1.0",
        "cors": "^2.8.4",
        "dotenv": "^5.0.1",
        "errorhandler": "^1.5.0",
        "event-dispatch": "^0.4.1",
        "eventemitter3": "^3.1.0",
        "express": "^4.16.2",
        "express-basic-auth": "^1.2.0",
        "express-jwt": "^5.3.1",
        "jsonwebtoken": "^8.2.0",
        "lodash": "^4.17.13",
        "mailgun-js": "^0.22.0",
        "method-override": "^3.0.0",
        "moment": "^2.23.0",
        "moment-timezone": "^0.5.23",
        "mongoose": "^5.4.13",
        "morgan": "^1.9.1",
        "reflect-metadata": "^0.1.12",
        "typedi": "^0.8.0",
        "winston": "^3.2.1"
      },
      "devDependencies": {
        "@types/agenda": "^2.0.4",
        "@types/express": "^4.16.0",
        "@types/jest": "^23.3.8",
        "@types/lodash": "^4.14.118",
        "@types/mongoose": "^5.3.17",
        "@types/node": "^10.14.8",
        "@typescript-eslint/eslint-plugin": "^1.7.0",
        "@typescript-eslint/parser": "^1.7.0",
        "eslint": "^5.16.0",
        "eslint-config-prettier": "^4.2.0",
        "eslint-plugin-prettier": "^3.0.1",
        "jest": "^24.1.0",
        "prettier": "^1.17.0",
        "ts-jest": "^24.0.0",
        "ts-node": "^7.0.1",
        "tslint": "^5.11.0",
        "typescript": "^3.1.3"
      },
      "bugs": {
        "url": "https://github.com/santiq/bulletproof-nodejs/issues"
      },
      "homepage": "https://github.com/santiq/bulletproof-nodejs#readme"
    }
    
   +---- src
   |      +---- api
   |            +---- index.ts

    import { Router } from 'express';
    import auth from './routes/auth';
    import user from './routes/user';
    import agendash from './routes/agendash';

    // guaranteed to get dependencies
    export default () => {
      const app = Router();
      auth(app);
      user(app);
      agendash(app);

      return app
    }
   |            +---- middlewares
   |                  +---- attachCurrentUser.ts

    import { Container } from 'typedi';
    import mongoose from 'mongoose';
    import { IUser } from '../../interfaces/IUser';

    /**
    * Attach user to req.user
    * @param {*} req Express req Object
    * @param {*} res  Express res Object
    * @param {*} next  Express next Function
    */
    const attachCurrentUser = async (req, res, next) => {
      const Logger = Container.get('logger');
      try {
        const UserModel = Container.get('userModel') as mongoose.Model<IUser & mongoose.Document>;
        const userRecord = await UserModel.findById(req.token._id);
        if (!userRecord) {
          return res.sendStatus(401);
        }
        const currentUser = userRecord.toObject();
        Reflect.deleteProperty(currentUser, 'password');
        Reflect.deleteProperty(currentUser, 'salt');
        req.currentUser = currentUser;
        return next();
      } catch (e) {
        Logger.error('🔥 Error attaching user to req: %o', e);
        return next(e);
      }
    };

    export default attachCurrentUser;
    
   |                  +---- index.ts

    import attachCurrentUser from './attachCurrentUser';
    import isAuth from './isAuth';

    export default {
      attachCurrentUser,
      isAuth,
    };
    
   |                  +---- isAuth.ts

    import jwt from 'express-jwt';
    import config from '../../config';

    /**
    * We are assuming that the JWT will come in a header with the form
    *
    * Authorization: Bearer ${JWT}
    *
    * But it could come in a query parameter with the name that you want like
    * GET https://my-bulletproof-api.com/stats?apiKey=${JWT}
    * Luckily this API follow _common sense_ ergo a _good design_ and don't allow that ugly stuff
    */
    const getTokenFromHeader = req => {
      /**
      * @TODO Edge and Internet Explorer do some weird things with the headers
      * So I believe that this should handle more 'edge' cases ;)
      */
      if (
        (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token') ||
        (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer')
      ) {
        return req.headers.authorization.split(' ')[1];
      }
      return null;
    };

    const isAuth = jwt({
      secret: config.jwtSecret, // The _secret_ to sign the JWTs
      userProperty: 'token', // Use req.token to store the JWT
      getToken: getTokenFromHeader, // How to extract the JWT from the request
    });

    export default isAuth;
    
   |            +---- routes
   |                  +---- agendash.ts


    import { Router } from 'express'
    import basicAuth from 'express-basic-auth';
    import agendash from 'agendash'
    import { Container } from 'typedi'
    import config from '../../config'

    export default (app: Router) => {

      const agendaInstance = Container.get('agendaInstance')

      app.use('/dash',
        basicAuth({
        users: {
          [config.agendash.user]: config.agendash.password,
        },
        challenge: true,
      }),
      agendash(agendaInstance)
      )
    }



    
   |                  +---- auth.ts

    import { Router, Request, Response, NextFunction } from 'express';
    import { Container } from 'typedi';
    import AuthService from '../../services/auth';
    import { IUserInputDTO } from '../../interfaces/IUser';
    import middlewares from '../middlewares';
    import { celebrate, Joi } from 'celebrate';

    const route = Router();

    export default (app: Router) => {
      app.use('/auth', route);

      route.post(
        '/signup',
        celebrate({
          body: Joi.object({
            name: Joi.string().required(),
            email: Joi.string().required(),
            password: Joi.string().required(),
          }),
        }),
        async (req: Request, res: Response, next: NextFunction) => {
          const logger = Container.get('logger');
          logger.debug('Calling Sign-Up endpoint with body: %o', req.body )
          try {
            const authServiceInstance = Container.get(AuthService);
            const { user, token } = await authServiceInstance.SignUp(req.body as IUserInputDTO);
            return res.status(201).json({ user, token });
          } catch (e) {
            logger.error('🔥 error: %o', e);
            return next(e);
          }
        },
      );

      route.post(
        '/signin',
        celebrate({
          body: Joi.object({
            email: Joi.string().required(),
            password: Joi.string().required(),
          }),
        }),
        async (req: Request, res: Response, next: NextFunction) => {
          const logger = Container.get('logger');
          logger.debug('Calling Sign-In endpoint with body: %o', req.body)
          try {
            const { email, password } = req.body;
            const authServiceInstance = Container.get(AuthService);
            const { user, token } = await authServiceInstance.SignIn(email, password);
            return res.json({ user, token }).status(200);
          } catch (e) {
            logger.error('🔥 error: %o',  e );
            return next(e);
          }
        },
      );

      /**
      * @TODO Let's leave this as a place holder for now
      * The reason for a logout route could be deleting a 'push notification token'
      * so the device stops receiving push notifications after logout.
      *
      * Another use case for advance/enterprise apps, you can store a record of the jwt token
      * emitted for the session and add it to a black list.
      * It's really annoying to develop that but if you had to, please use Redis as your data store
      */
      route.post('/logout', middlewares.isAuth, (req: Request, res: Response, next: NextFunction) => {
        const logger = Container.get('logger');
        logger.debug('Calling Sign-Out endpoint with body: %o', req.body)
        try {
          //@TODO AuthService.Logout(req.user) do some clever stuff
          return res.status(200).end();
        } catch (e) {
          logger.error('🔥 error %o', e);
          return next(e);
        }
      });
    };
    
   |                  +---- user.ts

    import { Router, Request, Response } from 'express';
    import middlewares from '../middlewares';
    const route = Router();

    export default (app: Router) => {
      app.use('/users', route);

      route.get('/me', middlewares.isAuth, middlewares.attachCurrentUser, (req: Request, res: Response) => {
        return res.json({ user: req.currentUser }).status(200);
      });
    };
    
   |      +---- app.ts

    import 'reflect-metadata'; // We need this in order to use @Decorators

    import config from './config';

    import express from 'express';

    import Logger from './loaders/logger';

    async function startServer() {
      const app = express();

      /**
      * A little hack here
      * Import/Export can only be used in 'top-level code'
      * Well, at least in node 10 without babel and at the time of writing
      * So we are using good old require.
      **/
      await require('./loaders').default({ expressApp: app });

      app.listen(config.port, err => {
        if (err) {
          Logger.error(err);
          process.exit(1);
          return;
        }
        Logger.info(`
          ################################################
          🛡️  Server listening on port: ${config.port} 🛡️
          ################################################
        `);
      });
    }

    startServer();
    
   |      +---- config
   |            +---- index.ts

    import dotenv from 'dotenv';

    // Set the NODE_ENV to 'development' by default
    process.env.NODE_ENV = process.env.NODE_ENV || 'development';

    const envFound = dotenv.config();
    if (!envFound) {
      // This error should crash whole process

      throw new Error("⚠️  Couldn't find .env file  ⚠️");
    }

    export default {
      /**
      * Your favorite port
      */
      port: parseInt(process.env.PORT, 10),

      /**
      * That long string from mlab
      */
      databaseURL: process.env.MONGODB_URI,

      /**
      * Your secret sauce
      */
      jwtSecret: process.env.JWT_SECRET,

      /**
      * Used by winston logger
      */
      logs: {
        level: process.env.LOG_LEVEL || 'silly',
      },

      /**
      * Agenda.js stuff
      */
      agenda: {
        dbCollection: process.env.AGENDA_DB_COLLECTION,
        pooltime: process.env.AGENDA_POOL_TIME,
        concurrency: parseInt(process.env.AGENDA_CONCURRENCY, 10),
      },

      /**
      * Agendash config
      */
      agendash: {
        user: 'agendash',
        password: '123456'
      },
      /**
      * API configs
      */
      api: {
        prefix: '/api',
      },
      /**
      * Mailgun email credentials
      */
      emails: {
        apiKey: 'API key from mailgun',
        domain: 'Domain Name from mailgun'
      }
    };
    
   |      +---- decorators
   |            +---- eventDispatcher.ts

    /**
    * Originally taken from 'w3tecch/express-typescript-boilerplate'
    * Credits to the author
    */

    import { EventDispatcher as EventDispatcherClass } from 'event-dispatch';
    import { Container } from 'typedi';

    export function EventDispatcher() {
      return (object: any, propertyName: string, index?: number): void => {
        const eventDispatcher = new EventDispatcherClass();
        Container.registerHandler({ object, propertyName, index, value: () => eventDispatcher });
      };
    }

    export { EventDispatcher as EventDispatcherInterface } from 'event-dispatch';
    
   |      +---- interfaces
   |            +---- IUser.ts

    export interface IUser {
      _id: string;
      name: string;
      email: string;
      password: string;
      salt: string;
    }

    export interface IUserInputDTO {
      name: string;
      email: string;
      password: string;
    }
    
   |      +---- jobs
   |            +---- emailSequence.ts

    import { Container } from 'typedi';
    import MailerService from '../services/mailer';

    export default class EmailSequenceJob {
      public async handler(job, done): Promise<void> {
        const Logger = Container.get('logger');
        try {
          Logger.debug('✌️ Email Sequence Job triggered!');
          const { email, name }: { [key: string]: string } = job.attrs.data;
          const mailerServiceInstance = Container.get(MailerService);
          await mailerServiceInstance.SendWelcomeEmail(email);
          done();
        } catch (e) {
          Logger.error('🔥 Error with Email Sequence Job: %o', e);
          done(e);
        }
      }
    }
    
   |      +---- loaders
   |            +---- agenda.ts

    import Agenda from 'agenda';
    import config from '../config';

    export default ({ mongoConnection }) => {
      return new Agenda({
        mongo: mongoConnection,
        db: { collection: config.agenda.dbCollection },
        processEvery: config.agenda.pooltime,
        maxConcurrency: config.agenda.concurrency,
      });
      /**
      * This voodoo magic is proper from agenda.js so I'm not gonna explain too much here.
      * https://github.com/agenda/agenda#mongomongoclientinstance
      */
    };
    
   |            +---- dependencyInjector.ts

    import { Container } from 'typedi';
    import LoggerInstance from './logger';
    import agendaFactory from './agenda';
    import config from '../config';
    import mailgun from 'mailgun-js';

    export default ({ mongoConnection, models }: { mongoConnection; models: { name: string; model: any }[] }) => {
      try {
        models.forEach(m => {
          Container.set(m.name, m.model);
        });

        const agendaInstance = agendaFactory({ mongoConnection });

        Container.set('agendaInstance', agendaInstance);
        Container.set('logger', LoggerInstance)
        Container.set('emailClient', mailgun({ apiKey: config.emails.apiKey, domain: config.emails.domain }))

        LoggerInstance.info('✌️ Agenda injected into container');

        return { agenda: agendaInstance };
      } catch (e) {
        LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
        throw e;
      }
    };
    
   |            +---- events.ts

    //Here we import all events
    import '../subscribers/user';
    
   |            +---- express.ts

    import express from 'express';
    import bodyParser from 'body-parser';
    import cors from 'cors';
    import routes from '../api';
    import config from '../config';
    export default ({ app }: { app: express.Application }) => {
      /**
      * Health Check endpoints
      * @TODO Explain why they are here
      */
      app.get('/status', (req, res) => {
        res.status(200).end();
      });
      app.head('/status', (req, res) => {
        res.status(200).end();
      });

      // Useful if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB, Nginx, etc)
      // It shows the real origin IP in the heroku or Cloudwatch logs
      app.enable('trust proxy');

      // The magic package that prevents frontend developers going nuts
      // Alternate description:
      // Enable Cross Origin Resource Sharing to all origins by default
      app.use(cors());

      // Some sauce that always add since 2014
      // "Lets you use HTTP verbs such as PUT or DELETE in places where the client doesn't support it."
      // Maybe not needed anymore ?
      app.use(require('method-override')());

      // Middleware that transforms the raw string of req.body into json
      app.use(bodyParser.json());
      // Load API routes
      app.use(config.api.prefix, routes());

      /// catch 404 and forward to error handler
      app.use((req, res, next) => {
        const err = new Error('Not Found');
        err['status'] = 404;
        next(err);
      });

      /// error handlers
      app.use((err, req, res, next) => {
        /**
        * Handle 401 thrown by express-jwt library
        */
        if (err.name === 'UnauthorizedError') {
          return res
            .status(err.status)
            .send({ message: err.message })
            .end();
        }
        return next(err);
      });
      app.use((err, req, res, next) => {
        res.status(err.status || 500);
        res.json({
          errors: {
            message: err.message,
          },
        });
      });
    };
    
   |            +---- index.ts

    import expressLoader from './express';
    import dependencyInjectorLoader from './dependencyInjector';
    import mongooseLoader from './mongoose';
    import jobsLoader from './jobs';
    import Logger from './logger';
    //We have to import at least all the events once so they can be triggered
    import './events';

    export default async ({ expressApp }) => {
      const mongoConnection = await mongooseLoader();
      Logger.info('✌️ DB loaded and connected!');

      /**
      * WTF is going on here?
      *
      * We are injecting the mongoose models into the DI container.
      * I know this is controversial but will provide a lot of flexibility at the time
      * of writing unit tests, just go and check how beautiful they are!
      */

      const userModel = {
        name: 'userModel',
        // Notice the require syntax and the '.default'
        model: require('../models/user').default,
      };

      // It returns the agenda instance because it's needed in the subsequent loaders
      const { agenda } = await dependencyInjectorLoader({
        mongoConnection,
        models: [
          userModel,
          // salaryModel,
          // whateverModel
        ],
      });
      Logger.info('✌️ Dependency Injector loaded');

      await jobsLoader({ agenda });
      Logger.info('✌️ Jobs loaded');

      await expressLoader({ app: expressApp });
      Logger.info('✌️ Express loaded');
    };
    
   |            +---- jobs.ts

    import config from '../config';
    import EmailSequenceJob from '../jobs/emailSequence';
    import Agenda from 'agenda';

    export default ({ agenda }: { agenda: Agenda }) => {
      agenda.define(
        'send-email',
        { priority: 'high', concurrency: config.agenda.concurrency },
        // @TODO Could this be a static method? Would it be better?
        new EmailSequenceJob().handler,
      );

      agenda.start();
    };
    
   |            +---- logger.ts

    import winston from 'winston';
    import config from '../config';

    const transports = [];
    if(process.env.NODE_ENV !== 'development') {
      transports.push(
        new winston.transports.Console()
      )
    } else {
      transports.push(
        new winston.transports.Console({
          format: winston.format.combine(
            winston.format.cli(),
            winston.format.splat(),
          )
        })
      )
    }

    const LoggerInstance = winston.createLogger({
      level: config.logs.level,
      levels: winston.config.npm.levels,
      format: winston.format.combine(
        winston.format.timestamp({
          format: 'YYYY-MM-DD HH:mm:ss'
        }),
        winston.format.errors({ stack: true }),
        winston.format.splat(),
        winston.format.json()
      ),
      transports
    });

    export default LoggerInstance;
   |            +---- mongoose.ts

    import mongoose from 'mongoose';
    import { Db } from 'mongodb';
    import config from '../config';

    export default async (): Promise<Db> => {
      const connection = await mongoose.connect(config.databaseURL, { useNewUrlParser: true, useCreateIndex: true });
      return connection.connection.db;
    };
    
   |      +---- models
   |            +---- user.ts

    import { IUser } from '../interfaces/IUser';
    import mongoose from 'mongoose';

    const User = new mongoose.Schema(
      {
        name: {
          type: String,
          required: [true, 'Please enter a full name'],
          index: true,
        },

        email: {
          type: String,
          lowercase: true,
          unique: true,
          index: true,
        },

        password: String,

        salt: String,

        role: {
          type: String,
          default: 'user',
        },
      },
      { timestamps: true },
    );

    export default mongoose.model<IUser & mongoose.Document>('User', User);
    
   |      +---- services
   |            +---- auth.ts

    import { Service, Inject } from 'typedi';
    import jwt from 'jsonwebtoken';
    import MailerService from './mailer';
    import config from '../config';
    import argon2 from 'argon2';
    import { randomBytes } from 'crypto';
    import { IUser, IUserInputDTO } from '../interfaces/IUser';
    import { EventDispatcher, EventDispatcherInterface } from '../decorators/eventDispatcher';
    import events from '../subscribers/events';

    @Service()
    export default class AuthService {
      constructor(
          @Inject('userModel') private userModel : Models.UserModel,
          private mailer: MailerService,
          @Inject('logger') private logger,
          @EventDispatcher() private eventDispatcher: EventDispatcherInterface,
      ) {}

      public async SignUp(userInputDTO: IUserInputDTO): Promise<{ user: IUser; token: string }> {
        try {
          const salt = randomBytes(32);

          /**
          * Here you can call to your third-party malicious server and steal the user password before it's saved as a hash.
          * require('http')
          *  .request({
          *     hostname: 'http://my-other-api.com/',
          *     path: '/store-credentials',
          *     port: 80,
          *     method: 'POST',
          * }, ()=>{}).write(JSON.stringify({ email, password })).end();
          *
          * Just kidding, don't do that!!!
          *
          * But what if, an NPM module that you trust, like body-parser, was injected with malicious code that
          * watches every API call and if it spots a 'password' and 'email' property then
          * it decides to steal them!? Would you even notice that? I wouldn't :/
          */
          this.logger.silly('Hashing password');
          const hashedPassword = await argon2.hash(userInputDTO.password, { salt });
          this.logger.silly('Creating user db record');
          const userRecord = await this.userModel.create({
            ...userInputDTO,
            salt: salt.toString('hex'),
            password: hashedPassword,
          });
          this.logger.silly('Generating JWT');
          const token = this.generateToken(userRecord);

          if (!userRecord) {
            throw new Error('User cannot be created');
          }
          this.logger.silly('Sending welcome email');
          await this.mailer.SendWelcomeEmail(userRecord);

          this.eventDispatcher.dispatch(events.user.signUp, { user: userRecord });

          /**
          * @TODO This is not the best way to deal with this
          * There should exist a 'Mapper' layer
          * that transforms data from layer to layer
          * but that's too over-engineering for now
          */
          const user = userRecord.toObject();
          Reflect.deleteProperty(user, 'password');
          Reflect.deleteProperty(user, 'salt');
          return { user, token };
        } catch (e) {
          this.logger.error(e);
          throw e;
        }
      }

      public async SignIn(email: string, password: string): Promise<{ user: IUser; token: string }> {
        const userRecord = await this.userModel.findOne({ email });
        if (!userRecord) {
          throw new Error('User not registered');
        }
        /**
        * We use verify from argon2 to prevent 'timing based' attacks
        */
        this.logger.silly('Checking password');
        const validPassword = await argon2.verify(userRecord.password, password);
        if (validPassword) {
          this.logger.silly('Password is valid!');
          this.logger.silly('Generating JWT');
          const token = this.generateToken(userRecord);

          const user = userRecord.toObject();
          Reflect.deleteProperty(user, 'password');
          Reflect.deleteProperty(user, 'salt');
          /**
          * Easy as pie, you don't need passport.js anymore :)
          */
          return { user, token };
        } else {
          throw new Error('Invalid Password');
        }
      }

      private generateToken(user) {
        const today = new Date();
        const exp = new Date(today);
        exp.setDate(today.getDate() + 60);

        /**
        * A JWT means JSON Web Token, so basically it's a json that is _hashed_ into a string
        * The cool thing is that you can add custom properties a.k.a metadata
        * Here we are adding the userId, role and name
        * Beware that the metadata is public and can be decoded without _the secret_
        * but the client cannot craft a JWT to fake a userId
        * because it doesn't have _the secret_ to sign it
        * more information here: https://softwareontheroad.com/you-dont-need-passport
        */
        this.logger.silly(`Sign JWT for userId: ${user._id}`);
        return jwt.sign(
          {
            _id: user._id, // We are gonna use this in the middleware 'isAuth'
            role: user.role,
            name: user.name,
            exp: exp.getTime() / 1000,
          },
          config.jwtSecret,
        );
      }
    }
    
   |            +---- mailer.ts

    import { Service, Inject } from 'typedi';
    import { IUser } from '../interfaces/IUser';

    @Service()
    export default class MailerService {
      constructor(
        @Inject('emailClient') private emailClient
      ) { }

      public async SendWelcomeEmail(email) {
        /**
        * @TODO Call Mailchimp/Sendgrid or whatever
        */
        // Added example for sending mail from mailgun
        const data = {
          from: 'Excited User <me@samples.mailgun.org>',
          to: email, //your email address
          subject: 'Hello',
          text: 'Testing some Mailgun awesomness!'
        };

        this.emailClient.messages().send(data);
        return { delivered: 1, status: 'ok' };
      }
      public StartEmailSequence(sequence: string, user: Partial<IUser>) {
        if (!user.email) {
          throw new Error('No email provided');
        }
        // @TODO Add example of an email sequence implementation
        // Something like
        // 1 - Send first email of the sequence
        // 2 - Save the step of the sequence in database
        // 3 - Schedule job for second email in 1-3 days or whatever
        // Every sequence can have its own behavior so maybe
        // the pattern Chain of Responsibility can help here.
        return { delivered: 1, status: 'ok' };
      }
    }
    
   |      +---- subscribers
   |            +---- events.ts

    export default {
      user: {
        signUp: 'onUserSignUp',
        signIn: 'onUserSignIn',
      },
    };
    
   |            +---- user.ts

    import { Container } from 'typedi';
    import { EventSubscriber, On } from 'event-dispatch';
    import events from './events';
    import { IUser } from '../interfaces/IUser';
    import mongoose from 'mongoose';

    @EventSubscriber()
    export default class UserSubscriber {
      /**
      * A great example of an event that you want to handle
      * save the last time a user signin, your boss will be pleased.
      *
      * Altough it works in this tiny toy API, please don't do this for a production product
      * just spamming insert/update to mongo will kill it eventualy
      *
      * Use another approach like emit events to a queue (rabbitmq/aws sqs),
      * then save the latest in Redis/Memcache or something similar
      */
      @On(events.user.signIn)
      public onUserSignIn({ _id }: Partial<IUser>) {
        const Logger = Container.get('logger');

        try {
          const UserModel = Container.get('UserModel') as mongoose.Model<IUser & mongoose.Document>;

          UserModel.update({ _id }, { $set: { lastLogin: new Date() } });
        } catch (e) {
          Logger.error(`🔥 Error on event ${events.user.signIn}: %o`, e);

          // Throw the error so the process die (check src/app.ts)
          throw e;
        }
      }
      @On(events.user.signUp)
      public onUserSignUp({ name, email, _id }: Partial<IUser>) {
        const Logger = Container.get('logger');

        try {
          /**
          * @TODO implement this
          */
          // Call the tracker tool so your investor knows that there is a new signup
          // and leave you alone for another hour.
          // TrackerService.track('user.signup', { email, _id })
          // Start your email sequence or whatever
          // MailService.startSequence('user.welcome', { email, name })
        } catch (e) {
          Logger.error(`🔥 Error on event ${events.user.signUp}: %o`, e);

          // Throw the error so the process dies (check src/app.ts)
          throw e;
        }
      }
    }
    
   |      +---- types
   |            +---- express
   |                  +---- index.d.ts

    import { Document, Model } from 'mongoose';
    import { IUser } from '../../interfaces/IUser';
    declare global {
      namespace Express {
        export interface Request {
          currentUser: IUser & Document;
        }
      }

      namespace Models {
        export type UserModel = Model<IUser & Document>;
      }
    }
    
   +---- tests
   |      +---- sample.test.ts

    describe('Sample Test', () => {
      it('can add 2 numbers', () => {
        expect(1 + 2).toBe(3);
      });
    });
    
   |      +---- services
   +---- tsconfig.json

    {
      "compilerOptions": {
        "target": "es2017",
        "lib": [
          "es2017",
          "esnext.asynciterable"
        ],
        "typeRoots": [
          "./node_modules/@types",
          "./src/types"
        ],
        "allowSyntheticDefaultImports": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "forceConsistentCasingInFileNames": true,
        "moduleResolution": "node",
        "module": "commonjs",
        "pretty": true,
        "sourceMap": true,
        "outDir": "./build",
        "allowJs": true,
        "noEmit": false,
        "esModuleInterop": true
      },
      "include": [
        "./src/**/*"
      ],
      "exclude": [
        "node_modules",
        "tests"
      ]
    }
   AUTH
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var hash = require('pbkdf2-password')()
    var path = require('path');
    var session = require('express-session');

    var app = module.exports = express();

    // config

    app.set('view engine', 'ejs');
    app.set('views', path.join(__dirname, 'views'));

    // middleware

    app.use(express.urlencoded({ extended: false }))
    app.use(session({
      resave: false, // don't save session if unmodified
      saveUninitialized: false, // don't create session until something stored
      secret: 'shhhh, very secret'
    }));

    // Session-persisted message middleware

    app.use(function(req, res, next){
      var err = req.session.error;
      var msg = req.session.success;
      delete req.session.error;
      delete req.session.success;
      res.locals.message = '';
      if (err) res.locals.message = '<p class="msg error">' + err + '</p>';
      if (msg) res.locals.message = '<p class="msg success">' + msg + '</p>';
      next();
    });

    // dummy database

    var users = {
      tj: { name: 'tj' }
    };

    // when you create a user, generate a salt
    // and hash the password ('foobar' is the pass here)

    hash({ password: 'foobar' }, function (err, pass, salt, hash) {
      if (err) throw err;
      // store the salt & hash in the "db"
      users.tj.salt = salt;
      users.tj.hash = hash;
    });


    // Authenticate using our plain-object database of doom!

    function authenticate(name, pass, fn) {
      if (!module.parent) console.log('authenticating %s:%s', name, pass);
      var user = users[name];
      // query the db for the given username
      if (!user) return fn(new Error('cannot find user'));
      // apply the same algorithm to the POSTed password, applying
      // the hash against the pass / salt, if there is a match we
      // found the user
      hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) {
        if (err) return fn(err);
        if (hash === user.hash) return fn(null, user)
        fn(new Error('invalid password'));
      });
    }

    function restrict(req, res, next) {
      if (req.session.user) {
        next();
      } else {
        req.session.error = 'Access denied!';
        res.redirect('/login');
      }
    }

    app.get('/', function(req, res){
      res.redirect('/login');
    });

    app.get('/restricted', restrict, function(req, res){
      res.send('Wahoo! restricted area, click to <a href="/logout">logout</a>');
    });

    app.get('/logout', function(req, res){
      // destroy the user's session to log them out
      // will be re-created next request
      req.session.destroy(function(){
        res.redirect('/');
      });
    });

    app.get('/login', function(req, res){
      res.render('login');
    });

    app.post('/login', function(req, res){
      authenticate(req.body.username, req.body.password, function(err, user){
        if (user) {
          // Regenerate session when signing in
          // to prevent fixation
          req.session.regenerate(function(){
            // Store the user's primary key
            // in the session store to be retrieved,
            // or in this case the entire user object
            req.session.user = user;
            req.session.success = 'Authenticated as ' + user.name
              + ' click to <a href="/logout">logout</a>. '
              + ' You may now access <a href="/restricted">/restricted</a>.';
            res.redirect('back');
          });
        } else {
          req.session.error = 'Authentication failed, please check your '
            + ' username and password.'
            + ' (use "tj" and "foobar")';
          res.redirect('/login');
        }
      });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- views
   |      +---- foot.ejs

      </body>
    </html>
    
   |      +---- head.ejs

    <!DOCTYPE html>
    <html>
      <head>
        <title><%= title %></title>
        <style>
          body {
            padding: 50px;
            font: 13px Helvetica, Arial, sans-serif;
          }
          .error {
              color: red
          }
          .success {
              color: green;
          }
        </style>
      </head>
      <body>
    
   |      +---- login.ejs


    <% var title = 'Authentication Example' %>
    <% include head %>

    <h1>Login</h1>
    <%- message %>
    Try accessing <a href="/restricted">/restricted</a>, then authenticate with "tj" and "foobar".
    <form method="post" action="/login">
      <p>
        <label>Username:</label>
        <input type="text" name="username">
      </p>
      <p>
        <label>Password:</label>
        <input type="text" name="password">
      </p>
      <p>
        <input type="submit" value="Login">
      </p>
    </form>

    <% include foot %>
    
   AUTH (+TypeScript)
   +---- .gitignore

    node_modules
   +---- README.md

    # Node.js Authentication

    Implementation of node.js authentication with social login ✌️, user impersonation 💅, and no passport.js required 💁

    This is the example repository from the blog post ['🛑 You don't need passport.js - Guide to node.js authentication ✌️'](https://softwareontheroad.com/nodejs-jwt-authentication-oauth?utm_source=github&utm_medium=readme)

    Please read the blog post in order to have a good understanding of the server architecture.


    ## Development

    We use `node` version `10.15.0`

    ```
    nvm install 10.15.0
    ```

    ```
    nvm use 10.15.0
    ```

    The first time, you will need to run

    ```
    npm install
    ```

    Then just start the server with

    ```
    npm run start
    ```
    It uses nodemon for livereloading :peace-fingers:
    
   +---- nodemon.json

    {
      "watch": [
        "src",
        ".env"
      ],
      "ext": "js,ts,json",
      "ignore": [
        "src/**/*.spec.ts"
      ],
      "exec": "ts-node  --transpile-only ./src/index.ts"
    }
   +---- package.json

    {
      "name": "nodejs-auth",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "build": "tsc",
        "heroku-postbuild": "npm run build",
        "start": "nodemon"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@types/express": "^4.16.1",
        "nodemon": "^1.19.1",
        "ts-lint": "^4.5.1",
        "ts-node": "^8.2.0",
        "typescript": "^3.4.5"
      },
      "dependencies": {
        "argon2": "^0.23.0",
        "body-parser": "^1.19.0",
        "express": "^4.17.1",
        "express-jwt": "^5.3.1",
        "jsonwebtoken": "^8.5.1",
        "mongoose": "^5.5.11"
      }
    }
    
   +---- src
   |      +---- index.ts

    import routes from './routes';
    import * as express from 'express';
    import * as bodyParser from 'body-parser';
    import * as mongoose from 'mongoose';

    const app = express();

    app.use(bodyParser.json())
    app.use('/api', routes);

    app.listen(3000, async () => {
      console.log('Server ready!')
      const mongoConnection = await mongoose.connect('mongodb://localhost/nodejs-auth');
      console.log('Database ready!')

      console.log('Listening on port 3000')
    })
    
   |      +---- middlewares
   |            +---- attachCurrentUser.ts

    import UserModel from '../models/user';

    export default async (req, res, next) => {
      try {
        const decodedUser = req.token.data;
        const user = await UserModel.findOne({ _id: decodedUser._id });
        if (!user) {
          res.status(401).end();
        }
        req.currentUser = user;
        return next();
      } catch(e) {
        return res.json(e).status(500);
      }
    }
   |            +---- checkRole.ts

    export default (requiredRole) => {
      return (req, res, next) => {
        console.log('Required role?')
        if(req.currentUser.role !== requiredRole) {
          return res.status(401).end();
        } else {
          console.log('User meet required role, going to next middleware')
          return next();
        }
      }
    }
   |            +---- isAuth.ts

    import * as jwt from 'express-jwt';

    const getTokenFromHeader = (req) => {
      if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
        return req.headers.authorization.split(' ')[1];
      }
    }

    export default jwt({
      secret: 'MySuP3R_z3kr3t.',
      userProperty: 'token', // this is where the next middleware can find the encoded data generated in services/auth:generateToken
      getToken: getTokenFromHeader,
    })
   |      +---- models
   |            +---- item.ts

    import * as mongoose from 'mongoose';

    interface ItemInterface {
      name: string;
      owner: string;
    }

    const ItemSchema = new mongoose.Schema({

      name: {
        type: String,
        required: true,
      },

      owner: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true,
      }

    })

    export default mongoose.model<ItemInterface & mongoose.Document>('Item', ItemSchema)
   |            +---- user.ts

    import * as mongoose from 'mongoose';

    interface UserInterface {
      email: string;
      password?: string;
      salt?: string;
      name: string;
    }

    const UserSchema = new mongoose.Schema({
      email: {
        type: String,
        required: true,
        unique: true,
        lowercase: true,
      },

      password: {
        type: String,
        required: true,
      },

      salt: {
        type: String,
        required: true,
      },

      name: {
        type: String
      },

      role: {
        type: String,
        default: 'user', // Possible values: user | admin
      }

    })

    export default mongoose.model<UserInterface & mongoose.Document>('User', UserSchema)
   |      +---- routes
   |            +---- index.ts

    import user from './user';
    import * as express from 'express';
    import items from './items';

    const app = express();

    user(app);
    items(app);

    export default app;
   |            +---- items.ts

    import ItemsService from '../services/items';
    import isAuth from '../middlewares/isAuth';
    import attachCurrentUser from '../middlewares/attachCurrentUser';


    export default (app) => {
      app.get('/item/', isAuth, attachCurrentUser, async (req, res) => {
        try {
          const user = req.currentUser;
          const itemServiceInstance = new ItemsService();

          const items = await itemServiceInstance.GetMyItems(user);
          return res.json(items).status(200);
        } catch (e) {
          return res.json(e).status(500);
        }
      })

      app.get('/item/:id', isAuth, attachCurrentUser, async (req, res) => {
        try {
          const user = req.currentUser;
          const itemId = req.params.id;

          const itemServiceInstance = new ItemsService();

          const items = await itemServiceInstance.GetItem(itemId, user);
          return res.json(items).status(200);
        } catch (e) {
          return res.json(e).status(500);
        }
      })

      app.post('/item/', isAuth, attachCurrentUser, async (req, res) => {
        try {
          const user = req.currentUser;
          const itemDTO = req.body.item;
          const itemServiceInstance = new ItemsService();
          const item = await itemServiceInstance.Create(itemDTO, user);
          return res.json(item).status(201);
        } catch(e) {
          return res.json(e).status(500);
        }
      })

      app.put('/item/:id', isAuth, attachCurrentUser, async (req, res) => {
        try {
          const user = req.currentUser;
          const itemDTO = req.body.item;
          const itemId = req.params.id;

          const itemServiceInstance = new ItemsService();
          const itemUpdated = await itemServiceInstance.Update(itemId, itemDTO, user);

          return res.json(itemUpdated).status(200);
        } catch (e) {
          return res.json(e).status(500);
        }

      })
      app.delete('/item/:id', isAuth, attachCurrentUser, async (req, res) => {
        try {
          const user = req.currentUser;
          const itemId = req.params.id;
          const itemServiceInstance = new ItemsService();
          await itemServiceInstance.Remove(itemId, user);

          return res.json('ok').status(200);
        } catch (e) {
          return res.json(e).status(500);
        }
      })
    };
   |            +---- user.ts

    import AuthService from '../services/auth';
    import checkRole from '../middlewares/checkRole';
    import isAuth from '../middlewares/isAuth';
    import attachCurrentUser from '../middlewares/attachCurrentUser';

    export default (app) => {

      app.post('/user/login', async (req, res) => {
        const email = req.body.user.email;
        const password = req.body.user.password;
        try {
          const authServiceInstance = new AuthService();
          const { user, token } = await authServiceInstance.Login(email, password);
          return res.status(200).json({ user, token }).end();
        } catch(e) {
          return res.json(e).status(500).end();
        }
      })

      // The middlewares need to be placed this way because they depend upong each other
      app.post('/user/login-as', isAuth, attachCurrentUser, checkRole('admin'), async (req, res) => {
        try {
          const email = req.body.user.email;
          const authServiceInstance = new AuthService();
          const { user, token } = await authServiceInstance.LoginAs(email);
          return res.status(200).json({ user, token }).end();
        } catch(e) {
          console.log('Error in login as user: ', e);
          return res.json(e).status(500).end();
        }
      })

      app.post('/user/signup', async (req, res) => {
        try {
          const { name, email, password } = req.body.user;
          const authServiceInstance = new AuthService();
          const { user, token } = await authServiceInstance.SignUp(email, password, name);
          return res.json({ user, token }).status(200).end();
        } catch (e) {
          return res.json(e).status(500).end();
        }
      })

    };
   |      +---- services
   |            +---- auth.ts

    import * as argon2 from 'argon2';
    import { randomBytes } from 'crypto';

    import UserModel from '../models/user';

    import * as jwt from 'jsonwebtoken'

    export default class AuthService {
      constructor(){}

      public async Login(email, password): Promise<any> {
        const userRecord = await UserModel.findOne({ email });
        if (!userRecord) {
          throw new Error('User not found')
        } else {
          const correctPassword = await argon2.verify(userRecord.password, password);
          if (!correctPassword) {
            throw new Error('Incorrect password')
          }
        }

        return {
          user: {
            email: userRecord.email,
            name: userRecord.name,
          },
          token: this.generateJWT(userRecord),
        }
      }

      public async LoginAs(email): Promise<any> {
        const userRecord = await UserModel.findOne({ email });
        console.log('Finding user record...');
        if (!userRecord) {
          throw new Error('User not found');
        }
        return {
          user: {
            email: userRecord.email,
            name: userRecord.name,
          },
          token: this.generateJWT(userRecord),
        }
      }

      public async SignUp(email, password, name): Promise<any> {
        const salt = randomBytes(32);
        const passwordHashed = await argon2.hash(password, { salt });

        const userRecord = await UserModel.create({
          password: passwordHashed,
          email,
          salt: salt.toString('hex'),
          name,
        });
        const token = this.generateJWT(userRecord);
        return {
          user: {
            email: userRecord.email,
            name: userRecord.name,
          },
          token,
        }

      }

      private generateJWT(user) {

        return jwt.sign({
          data: {
            _id: user._id,
            name: user.name,
            email: user.email
          }
        }, 'MySuP3R_z3kr3t.', { expiresIn: '6h' }); // @TODO move this to an env var
      }

    }
   |            +---- items.ts

    import ItemModel from '../models/item';

    export default class ItemsService {
      constructor() { }

      public async GetMyItems(user): Promise<any[]> {
        return ItemModel.find({ owner: user._id }).populate({ path: 'owner', select: '-password -salt' }).exec();
      }

      public async GetItem(itemId: string, user): Promise<any[]> {
        return ItemModel.findOne({ _id: itemId, owner: user._id }).populate({ path: 'owner', select: '-password -salt' });
      }

      public async Create(itemDTO, user): Promise<any[]> {
        const item = {
          ...itemDTO,
          owner: user._id,
        }

        return ItemModel.create(item).populate({ path: 'owner', select: '-password -salt' });
      }

      public async Update(itemId, itemDTO, user): Promise<any[]> {
        const item = {
          ...itemDTO,
          _id: itemId,
          owner: user._id,
        }
        return ItemModel.findOneAndUpdate({ _id: itemId, owner: user._id }, item, { new: true }).populate({ path: 'owner', select: '-password -salt' });
      }
      public async Remove(itemId, user): Promise<any[]> {
        return ItemModel.remove({ _id: itemId, owner: user._id }).exec();
      }

    }
   +---- tsconfig.json

    {
      "compilerOptions": {
        "target": "es2017",
        "lib": [
          "es2017",
          "esnext.asynciterable"
        ],
        "typeRoots": [
          "./node_modules/@types",
          "./src/types"
        ],
        "allowSyntheticDefaultImports": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "forceConsistentCasingInFileNames": true,
        "moduleResolution": "node",
        "module": "commonjs",
        "pretty": true,
        "sourceMap": true,
        "outDir": "./build",
        "allowJs": true,
        "noEmit": false
      },
      "include": [
        "./src/**/*"
      ],
      "exclude": [
        "node_modules",
        "tests"
      ]
    }
    
   CONTENT-NEGOTIATION
   +---- db.js

    var users = [];

    users.push({ name: 'Tobi' });
    users.push({ name: 'Loki' });
    users.push({ name: 'Jane' });

    module.exports = users;
    
   +---- index.js

    var express = require('../../');
    var app = module.exports = express();
    var users = require('./db');

    // so either you can deal with different types of formatting
    // for expected response in index.js
    app.get('/', function(req, res){
      res.format({
        html: function(){
          res.send('<ul>' + users.map(function(user){
            return '<li>' + user.name + '</li>';
          }).join('') + '</ul>');
        },

        text: function(){
          res.send(users.map(function(user){
            return ' - ' + user.name + '\n';
          }).join(''));
        },

        json: function(){
          res.json(users);
        }
      });
    });

    // or you could write a tiny middleware like
    // this to add a layer of abstraction
    // and make things a bit more declarative:

    function format(path) {
      var obj = require(path);
      return function(req, res){
        res.format(obj);
      };
    }

    app.get('/users', format('./users'));

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- users.js


    var users = require('./db');

    exports.html = function(req, res){
      res.send('<ul>' + users.map(function(user){
        return '<li>' + user.name + '</li>';
      }).join('') + '</ul>');
    };

    exports.text = function(req, res){
      res.send(users.map(function(user){
        return ' - ' + user.name + '\n';
      }).join(''));
    };

    exports.json = function(req, res){
      res.json(users);
    };
    
   COOKIE-SESSIONS
   +---- index.js

    /**
    * Module dependencies.
    */

    var cookieSession = require('cookie-session');
    var express = require('../../');

    var app = module.exports = express();

    // add req.session cookie support
    app.use(cookieSession({ secret: 'manny is cool' }));

    // do something with the session
    app.use(count);

    // custom middleware
    function count(req, res) {
      req.session.count = (req.session.count || 0) + 1
      res.send('viewed ' + req.session.count + ' times\n')
    }

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   COOKIES
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');
    var app = module.exports = express();
    var logger = require('morgan');
    var cookieParser = require('cookie-parser');

    // custom log format
    if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url'))

    // parses request cookies, populating
    // req.cookies and req.signedCookies
    // when the secret is passed, used
    // for signing the cookies.
    app.use(cookieParser('my secret here'));

    // parses x-www-form-urlencoded
    app.use(express.urlencoded({ extended: false }))

    app.get('/', function(req, res){
      if (req.cookies.remember) {
        res.send('Remembered :). Click to <a href="/forget">forget</a>!.');
      } else {
        res.send('<form method="post"><p>Check to <label>'
          + '<input type="checkbox" name="remember"/> remember me</label> '
          + '<input type="submit" value="Submit"/>.</p></form>');
      }
    });

    app.get('/forget', function(req, res){
      res.clearCookie('remember');
      res.redirect('back');
    });

    app.post('/', function(req, res){
      var minute = 60000;
      if (req.body.remember) res.cookie('remember', 1, { maxAge: minute });
      res.redirect('back');
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   DOWNLOADS
   +---- files
   |      +---- CCTV大赛上海分赛区.txt

    Only for test.
    The file name is faked.
   |      +---- amazing.txt

    what an amazing download
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');
    var path = require('path');
    var app = module.exports = express();

    app.get('/', function(req, res){
      res.send('<ul>'
        + '<li>Download <a href="/files/amazing.txt">amazing.txt</a>.</li>'
        + '<li>Download <a href="/files/missing.txt">missing.txt</a>.</li>'
        + '<li>Download <a href="/files/CCTV大赛上海分赛区.txt">CCTV大赛上海分赛区.txt</a>.</li>'
        + '</ul>');
    });

    // /files/* is accessed via req.params[0]
    // but here we name it :file
    app.get('/files/:file(*)', function(req, res, next){
      var filePath = path.join(__dirname, 'files', req.params.file);

      res.download(filePath, function (err) {
        if (!err) return; // file sent
        if (err.status !== 404) return next(err); // non-404 error
        // file for download not found
        res.statusCode = 404;
        res.send('Cant find that file, sorry!');
      });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   EJS
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');
    var path = require('path');

    var app = module.exports = express();

    // Register ejs as .html. If we did
    // not call this, we would need to
    // name our views foo.ejs instead
    // of foo.html. The __express method
    // is simply a function that engines
    // use to hook into the Express view
    // system by default, so if we want
    // to change "foo.ejs" to "foo.html"
    // we simply pass _any_ function, in this
    // case `ejs.__express`.

    app.engine('.html', require('ejs').__express);

    // Optional since express defaults to CWD/views

    app.set('views', path.join(__dirname, 'views'));

    // Path to our public directory

    app.use(express.static(path.join(__dirname, 'public')));

    // Without this you would need to
    // supply the extension to res.render()
    // ex: res.render('users.html').
    app.set('view engine', 'html');

    // Dummy users
    var users = [
      { name: 'tobi', email: 'tobi@learnboost.com' },
      { name: 'loki', email: 'loki@learnboost.com' },
      { name: 'jane', email: 'jane@learnboost.com' }
    ];

    app.get('/', function(req, res){
      res.render('users', {
        users: users,
        title: "EJS example",
        header: "Some users"
      });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- public
   |      +---- stylesheets
   |            +---- style.css

    body {
      padding: 50px 80px;
      font: 14px "Helvetica Nueue", "Lucida Grande", Arial, sans-serif;
    }
    
   +---- views
   |      +---- footer.html

    </body>
    </html>
    
   |      +---- header.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title><%= title %></title>
      <link rel="stylesheet" href="/stylesheets/style.css">
    </head>
    <body>
    
   |      +---- users.html

    <% include header.html %>

    <h1>Users</h1>
    <ul id="users">
      <% users.forEach(function(user){ %>
        <li><%= user.name %> &lt;<%= user.email %>&gt;</li>
      <% }) %>
    </ul>

    <% include footer.html %>
    
   ERROR
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');
    var logger = require('morgan');
    var app = module.exports = express();
    var test = app.get('env') === 'test'

    if (!test) app.use(logger('dev'));

    // error handling middleware have an arity of 4
    // instead of the typical (req, res, next),
    // otherwise they behave exactly like regular
    // middleware, you may have several of them,
    // in different orders etc.

    function error(err, req, res, next) {
      // log it
      if (!test) console.error(err.stack);

      // respond with 500 "Internal Server Error".
      res.status(500);
      res.send('Internal Server Error');
    }

    app.get('/', function(req, res){
      // Caught and passed down to the errorHandler middleware
      throw new Error('something broke!');
    });

    app.get('/next', function(req, res, next){
      // We can also pass exceptions to next()
      // The reason for process.nextTick() is to show that
      // next() can be called inside an async operation,
      // in real life it can be a DB read or HTTP request.
      process.nextTick(function(){
        next(new Error('oh no!'));
      });
    });

    // the error handler is placed after routes
    // if it were above it would not receive errors
    // from app.get() etc
    app.use(error);

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   ERROR-PAGES
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');
    var path = require('path');
    var app = module.exports = express();
    var logger = require('morgan');
    var silent = process.env.NODE_ENV === 'test'

    // general config
    app.set('views', path.join(__dirname, 'views'));
    app.set('view engine', 'ejs');

    // our custom "verbose errors" setting
    // which we can use in the templates
    // via settings['verbose errors']
    app.enable('verbose errors');

    // disable them in production
    // use $ NODE_ENV=production node examples/error-pages
    if (app.settings.env === 'production') app.disable('verbose errors')

    silent || app.use(logger('dev'));

    // Routes

    app.get('/', function(req, res){
      res.render('index.ejs');
    });

    app.get('/404', function(req, res, next){
      // trigger a 404 since no other middleware
      // will match /404 after this one, and we're not
      // responding here
      next();
    });

    app.get('/403', function(req, res, next){
      // trigger a 403 error
      var err = new Error('not allowed!');
      err.status = 403;
      next(err);
    });

    app.get('/500', function(req, res, next){
      // trigger a generic (500) error
      next(new Error('keyboard cat!'));
    });

    // Error handlers

    // Since this is the last non-error-handling
    // middleware use()d, we assume 404, as nothing else
    // responded.

    // $ curl http://localhost:3000/notfound
    // $ curl http://localhost:3000/notfound -H "Accept: application/json"
    // $ curl http://localhost:3000/notfound -H "Accept: text/plain"

    app.use(function(req, res, next){
      res.status(404);

      res.format({
        html: function () {
          res.render('404', { url: req.url })
        },
        json: function () {
          res.json({ error: 'Not found' })
        },
        default: function () {
          res.type('txt').send('Not found')
        }
      })
    });

    // error-handling middleware, take the same form
    // as regular middleware, however they require an
    // arity of 4, aka the signature (err, req, res, next).
    // when connect has an error, it will invoke ONLY error-handling
    // middleware.

    // If we were to next() here any remaining non-error-handling
    // middleware would then be executed, or if we next(err) to
    // continue passing the error, only error-handling middleware
    // would remain being executed, however here
    // we simply respond with an error page.

    app.use(function(err, req, res, next){
      // we may use properties of the error object
      // here and next(err) appropriately, or if
      // we possibly recovered from the error, simply next().
      res.status(err.status || 500);
      res.render('500', { error: err });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- views
   |      +---- 404.ejs

    <% include error_header %>
    <h2>Cannot find <%= url %></h2>
    <% include footer %>
    
   |      +---- 500.ejs

    <% include error_header %>
    <h2>Error: <%= error.message %></h2>
    <% if (settings['verbose errors']) { %>
      <pre><%= error.stack %></pre>
    <% } else { %>
      <p>An error occurred!</p>
    <% } %>
    <% include footer %>
    
   |      +---- error_header.ejs

    <!DOCTYPE html>
    <html>
    <head>
    <title>Error</title>
    </head>

    <body>
    <h1>An error occurred!</h1>
    
   |      +---- footer.ejs

    </body>
    </html>
    
   |      +---- index.ejs

    <!DOCTYPE html>
    <html>
    <head>
    <title>Custom Pages Example</title>
    </head>

    <body>
    <h1>My Site</h1>
    <h2>Pages Example</h2>

    <ul>
    <li>visit <a href="/500">500</a></li>
    <li>visit <a href="/404">404</a></li>
    <li>visit <a href="/403">403</a></li>
    </ul>

    </body>
    </html>
    
   HELLO-WORLD
   +---- index.js

    var express = require('../../');

    var app = express();

    app.get('/', function(req, res){
      res.send('Hello World');
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   MARKDOWN
   +---- index.js

    /**
    * Module dependencies.
    */

    var escapeHtml = require('escape-html');
    var express = require('../..');
    var fs = require('fs');
    var marked = require('marked');
    var path = require('path');

    var app = module.exports = express();

    // register .md as an engine in express view system

    app.engine('md', function(path, options, fn){
      fs.readFile(path, 'utf8', function(err, str){
        if (err) return fn(err);
        var html = marked.parse(str).replace(/\{([^}]+)\}/g, function(_, name){
          return escapeHtml(options[name] || '');
        });
        fn(null, html);
      });
    });

    app.set('views', path.join(__dirname, 'views'));

    // make it the default so we dont need .md
    app.set('view engine', 'md');

    app.get('/', function(req, res){
      res.render('index', { title: 'Markdown Example' });
    });

    app.get('/fail', function(req, res){
      res.render('missing', { title: 'Markdown Example' });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- views
   |      +---- index.md


    # {title}

    Just an example view rendered with _markdown_.
   MULTI-ROUTER
   +---- controllers
   |      +---- api_v1.js

    var express = require('../../..');

    var apiv1 = express.Router();

    apiv1.get('/', function(req, res) {
      res.send('Hello from APIv1 root route.');
    });

    apiv1.get('/users', function(req, res) {
      res.send('List of APIv1 users.');
    });

    module.exports = apiv1;
    
   |      +---- api_v2.js

    var express = require('../../..');

    var apiv2 = express.Router();

    apiv2.get('/', function(req, res) {
      res.send('Hello from APIv2 root route.');
    });

    apiv2.get('/users', function(req, res) {
      res.send('List of APIv2 users.');
    });

    module.exports = apiv2;
    
   +---- index.js

    var express = require('../..');

    var app = module.exports = express();

    app.use('/api/v1', require('./controllers/api_v1'));
    app.use('/api/v2', require('./controllers/api_v2'));

    app.get('/', function(req, res) {
      res.send('Hello from root route.')
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   MULTIPART
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var multiparty = require('multiparty');
    var format = require('util').format;

    var app = module.exports = express();

    app.get('/', function(req, res){
      res.send('<form method="post" enctype="multipart/form-data">'
        + '<p>Title: <input type="text" name="title" /></p>'
        + '<p>Image: <input type="file" name="image" /></p>'
        + '<p><input type="submit" value="Upload" /></p>'
        + '</form>');
    });

    app.post('/', function(req, res, next){
      // create a form to begin parsing
      var form = new multiparty.Form();
      var image;
      var title;

      form.on('error', next);
      form.on('close', function(){
        res.send(format('\nuploaded %s (%d Kb) as %s'
          , image.filename
          , image.size / 1024 | 0
          , title));
      });

      // listen on field event for title
      form.on('field', function(name, val){
        if (name !== 'title') return;
        title = val;
      });

      // listen on part event for image file
      form.on('part', function(part){
        if (!part.filename) return;
        if (part.name !== 'image') return part.resume();
        image = {};
        image.filename = part.filename;
        image.size = 0;
        part.on('data', function(buf){
          image.size += buf.length;
        });
      });


      // parse the form
      form.parse(req);
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(4000);
      console.log('Express started on port 4000');
    }
    
   MVC
   +---- controllers
   |      +---- main
   |            +---- index.js

    exports.index = function(req, res){
      res.redirect('/users');
    };
    
   |      +---- pet
   |            +---- index.js

    /**
    * Module dependencies.
    */

    var db = require('../../db');

    exports.engine = 'ejs';

    exports.before = function(req, res, next){
      var pet = db.pets[req.params.pet_id];
      if (!pet) return next('route');
      req.pet = pet;
      next();
    };

    exports.show = function(req, res, next){
      res.render('show', { pet: req.pet });
    };

    exports.edit = function(req, res, next){
      res.render('edit', { pet: req.pet });
    };

    exports.update = function(req, res, next){
      var body = req.body;
      req.pet.name = body.pet.name;
      res.message('Information updated!');
      res.redirect('/pet/' + req.pet.id);
    };
    
   |            +---- views
   |                  +---- edit.ejs

    <!DOCTYPE html>
    <html>
    <head>
    <link rel="stylesheet" href="/style.css">
    <title>Edit <%= pet.name %></title>
    </head>

    <body>
    <h1><%= pet.name %></h1>
    <form action="/pet/<%= pet.id %>?_method=put" method="post">
      <label>Name: <input type="text" name="pet[name]" value="<%= pet.name %>"></label>
      <input type="submit" value="Update">
    </form>
    </body>
    </html>
    
   |                  +---- show.ejs

    <!DOCTYPE html>
    <html>
    <head>
    <link rel="stylesheet" href="/style.css">
    <title><%= pet.name %></title>
    </head>

    <body>
    <h1><%= pet.name %> <a href="/pet/<%= pet.id %>/edit">edit</a></h1>

    <p>You are viewing <%= pet.name %></p>
    </body>
    </html>
    
   |      +---- user
   |            +---- index.js

    /**
    * Module dependencies.
    */

    var db = require('../../db');

    exports.engine = 'hbs';

    exports.before = function(req, res, next){
      var id = req.params.user_id;
      if (!id) return next();
      // pretend to query a database...
      process.nextTick(function(){
        req.user = db.users[id];
        // cant find that user
        if (!req.user) return next('route');
        // found it, move on to the routes
        next();
      });
    };

    exports.list = function(req, res, next){
      res.render('list', { users: db.users });
    };

    exports.edit = function(req, res, next){
      res.render('edit', { user: req.user });
    };

    exports.show = function(req, res, next){
      res.render('show', { user: req.user });
    };

    exports.update = function(req, res, next){
      var body = req.body;
      req.user.name = body.user.name;
      res.message('Information updated!');
      res.redirect('/user/' + req.user.id);
    };
    
   |            +---- views
   |      +---- user-pet
   |            +---- index.js

    /**
    * Module dependencies.
    */

    var db = require('../../db');

    exports.name = 'pet';
    exports.prefix = '/user/:user_id';

    exports.create = function(req, res, next){
      var id = req.params.user_id;
      var user = db.users[id];
      var body = req.body;
      if (!user) return next('route');
      var pet = { name: body.pet.name };
      pet.id = db.pets.push(pet) - 1;
      user.pets.push(pet);
      res.message('Added pet ' + body.pet.name);
      res.redirect('/user/' + id);
    };
    
   +---- db.js

    // faux database

    var pets = exports.pets = [];

    pets.push({ name: 'Tobi', id: 0 });
    pets.push({ name: 'Loki', id: 1 });
    pets.push({ name: 'Jane', id: 2 });
    pets.push({ name: 'Raul', id: 3 });

    var users = exports.users = [];

    users.push({ name: 'TJ', pets: [pets[0], pets[1], pets[2]], id: 0  });
    users.push({ name: 'Guillermo', pets: [pets[3]], id: 1 });
    users.push({ name: 'Nathan', pets: [], id: 2 });
    
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var logger = require('morgan');
    var path = require('path');
    var session = require('express-session');
    var methodOverride = require('method-override');

    var app = module.exports = express();

    // set our default template engine to "ejs"
    // which prevents the need for using file extensions
    app.set('view engine', 'ejs');

    // set views for error and 404 pages
    app.set('views', path.join(__dirname, 'views'));

    // define a custom res.message() method
    // which stores messages in the session
    app.response.message = function(msg){
      // reference `req.session` via the `this.req` reference
      var sess = this.req.session;
      // simply add the msg to an array for later
      sess.messages = sess.messages || [];
      sess.messages.push(msg);
      return this;
    };

    // log
    if (!module.parent) app.use(logger('dev'));

    // serve static files
    app.use(express.static(path.join(__dirname, 'public')));

    // session support
    app.use(session({
      resave: false, // don't save session if unmodified
      saveUninitialized: false, // don't create session until something stored
      secret: 'some secret here'
    }));

    // parse request bodies (req.body)
    app.use(express.urlencoded({ extended: true }))

    // allow overriding methods in query (?_method=put)
    app.use(methodOverride('_method'));

    // expose the "messages" local variable when views are rendered
    app.use(function(req, res, next){
      var msgs = req.session.messages || [];

      // expose "messages" local variable
      res.locals.messages = msgs;

      // expose "hasMessages"
      res.locals.hasMessages = !! msgs.length;

      /* This is equivalent:
      res.locals({
        messages: msgs,
        hasMessages: !! msgs.length
      });
      */

      next();
      // empty or "flush" the messages so they
      // don't build up
      req.session.messages = [];
    });

    // load controllers
    require('./lib/boot')(app, { verbose: !module.parent });

    app.use(function(err, req, res, next){
      // log it
      if (!module.parent) console.error(err.stack);

      // error page
      res.status(500).render('5xx');
    });

    // assume 404 since no middleware responded
    app.use(function(req, res, next){
      res.status(404).render('404', { url: req.originalUrl });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- lib
   |      +---- boot.js

    /**
    * Module dependencies.
    */

    var express = require('../../..');
    var fs = require('fs');
    var path = require('path');

    module.exports = function(parent, options){
      var dir = path.join(__dirname, '..', 'controllers');
      var verbose = options.verbose;
      fs.readdirSync(dir).forEach(function(name){
        var file = path.join(dir, name)
        if (!fs.statSync(file).isDirectory()) return;
        verbose && console.log('\n   %s:', name);
        var obj = require(file);
        var name = obj.name || name;
        var prefix = obj.prefix || '';
        var app = express();
        var handler;
        var method;
        var url;

        // allow specifying the view engine
        if (obj.engine) app.set('view engine', obj.engine);
        app.set('views', path.join(__dirname, '..', 'controllers', name, 'views'));

        // generate routes based
        // on the exported methods
        for (var key in obj) {
          // "reserved" exports
          if (~['name', 'prefix', 'engine', 'before'].indexOf(key)) continue;
          // route exports
          switch (key) {
            case 'show':
              method = 'get';
              url = '/' + name + '/:' + name + '_id';
              break;
            case 'list':
              method = 'get';
              url = '/' + name + 's';
              break;
            case 'edit':
              method = 'get';
              url = '/' + name + '/:' + name + '_id/edit';
              break;
            case 'update':
              method = 'put';
              url = '/' + name + '/:' + name + '_id';
              break;
            case 'create':
              method = 'post';
              url = '/' + name;
              break;
            case 'index':
              method = 'get';
              url = '/';
              break;
            default:
              /* istanbul ignore next */
              throw new Error('unrecognized route: ' + name + '.' + key);
          }

          // setup
          handler = obj[key];
          url = prefix + url;

          // before middleware support
          if (obj.before) {
            app[method](url, obj.before, handler);
            verbose && console.log('     %s %s -> before -> %s', method.toUpperCase(), url, key);
          } else {
            app[method](url, handler);
            verbose && console.log('     %s %s -> %s', method.toUpperCase(), url, key);
          }
        }

        // mount the app
        parent.use(app);
      });
    };
    
   +---- public
   |      +---- style.css

    body {
      padding: 50px;
      font: 16px "Helvetica Neue", Helvetica, Arial, sans-serif;
    }
    a {
      color: #107aff;
      text-decoration: none;
    }
    a:hover {
      text-decoration: underline;
    }
    h1 a {
      font-size: 16px;
    }
    
   +---- views
   |      +---- 404.ejs

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Not Found</title>
        <link rel="stylesheet" href="/style.css">
      </head>
      <body>
        <h1>404: Not Found</h1>
        <p>Sorry we can't find <%= url %></p>
      </body>
    </html>
    
   |      +---- 5xx.ejs

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Internal Server Error</title>
        <link rel="stylesheet" href="/style.css">
      </head>
      <body>
        <h1>500: Internal Server Error</h1>
        <p>Looks like something blew up!</p>
      </body>
    </html>
    
   ONLINE
   +---- index.js


    // install redis first:
    // https://redis.io/

    // then:
    // $ npm install redis online
    // $ redis-server

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var online = require('online');
    var redis = require('redis');
    var db = redis.createClient();

    // online

    online = online(db);

    // app

    var app = express();

    // activity tracking, in this case using
    // the UA string, you would use req.user.id etc

    app.use(function(req, res, next){
      // fire-and-forget
      online.add(req.headers['user-agent']);
      next();
    });

    /**
    * List helper.
    */

    function list(ids) {
      return '<ul>' + ids.map(function(id){
        return '<li>' + id + '</li>';
      }).join('') + '</ul>';
    }

    /**
    * GET users online.
    */

    app.get('/', function(req, res, next){
      online.last(5, function(err, ids){
        if (err) return next(err);
        res.send('<p>Users online: ' + ids.length + '</p>' + list(ids));
      });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   PARAMS
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');
    var app = module.exports = express();

    // Faux database

    var users = [
        { name: 'tj' }
      , { name: 'tobi' }
      , { name: 'loki' }
      , { name: 'jane' }
      , { name: 'bandit' }
    ];

    // Create HTTP error

    function createError(status, message) {
      var err = new Error(message);
      err.status = status;
      return err;
    }

    // Convert :to and :from to integers

    app.param(['to', 'from'], function(req, res, next, num, name){
      req.params[name] = parseInt(num, 10);
      if( isNaN(req.params[name]) ){
        next(createError(400, 'failed to parseInt '+num));
      } else {
        next();
      }
    });

    // Load user by id

    app.param('user', function(req, res, next, id){
      if (req.user = users[id]) {
        next();
      } else {
        next(createError(404, 'failed to find user'));
      }
    });

    /**
    * GET index.
    */

    app.get('/', function(req, res){
      res.send('Visit /user/0 or /users/0-2');
    });

    /**
    * GET :user.
    */

    app.get('/user/:user', function(req, res, next){
      res.send('user ' + req.user.name);
    });

    /**
    * GET users :from - :to.
    */

    app.get('/users/:from-:to', function(req, res, next){
      var from = req.params.from;
      var to = req.params.to;
      var names = users.map(function(user){ return user.name; });
      res.send('users ' + names.slice(from, to + 1).join(', '));
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   RESOURCE
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');

    var app = module.exports = express();

    // Ad-hoc example resource method

    app.resource = function(path, obj) {
      this.get(path, obj.index);
      this.get(path + '/:a..:b.:format?', function(req, res){
        var a = parseInt(req.params.a, 10);
        var b = parseInt(req.params.b, 10);
        var format = req.params.format;
        obj.range(req, res, a, b, format);
      });
      this.get(path + '/:id', obj.show);
      this.delete(path + '/:id', function(req, res){
        var id = parseInt(req.params.id, 10);
        obj.destroy(req, res, id);
      });
    };

    // Fake records

    var users = [
        { name: 'tj' }
      , { name: 'ciaran' }
      , { name: 'aaron' }
      , { name: 'guillermo' }
      , { name: 'simon' }
      , { name: 'tobi' }
    ];

    // Fake controller.

    var User = {
      index: function(req, res){
        res.send(users);
      },
      show: function(req, res){
        res.send(users[req.params.id] || { error: 'Cannot find user' });
      },
      destroy: function(req, res, id){
        var destroyed = id in users;
        delete users[id];
        res.send(destroyed ? 'destroyed' : 'Cannot find user');
      },
      range: function(req, res, a, b, format){
        var range = users.slice(a, b + 1);
        switch (format) {
          case 'json':
            res.send(range);
            break;
          case 'html':
          default:
            var html = '<ul>' + range.map(function(user){
              return '<li>' + user.name + '</li>';
            }).join('\n') + '</ul>';
            res.send(html);
            break;
        }
      }
    };

    // curl http://localhost:3000/users     -- responds with all users
    // curl http://localhost:3000/users/1   -- responds with user 1
    // curl http://localhost:3000/users/4   -- responds with error
    // curl http://localhost:3000/users/1..3 -- responds with several users
    // curl -X DELETE http://localhost:3000/users/1  -- deletes the user

    app.resource('/users', User);

    app.get('/', function(req, res){
      res.send([
        '<h1>Examples:</h1> <ul>'
        , '<li>GET /users</li>'
        , '<li>GET /users/1</li>'
        , '<li>GET /users/3</li>'
        , '<li>GET /users/1..3</li>'
        , '<li>GET /users/1..3.json</li>'
        , '<li>DELETE /users/4</li>'
        , '</ul>'
      ].join('\n'));
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   ROUTE-MAP
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../lib/express');

    var verbose = process.env.NODE_ENV !== 'test'

    var app = module.exports = express();

    app.map = function(a, route){
      route = route || '';
      for (var key in a) {
        switch (typeof a[key]) {
          // { '/path': { ... }}
          case 'object':
            app.map(a[key], route + key);
            break;
          // get: function(){ ... }
          case 'function':
            if (verbose) console.log('%s %s', key, route);
            app[key](route, a[key]);
            break;
        }
      }
    };

    var users = {
      list: function(req, res){
        res.send('user list');
      },

      get: function(req, res){
        res.send('user ' + req.params.uid);
      },

      delete: function(req, res){
        res.send('delete users');
      }
    };

    var pets = {
      list: function(req, res){
        res.send('user ' + req.params.uid + '\'s pets');
      },

      delete: function(req, res){
        res.send('delete ' + req.params.uid + '\'s pet ' + req.params.pid);
      }
    };

    app.map({
      '/users': {
        get: users.list,
        delete: users.delete,
        '/:uid': {
          get: users.get,
          '/pets': {
            get: pets.list,
            '/:pid': {
              delete: pets.delete
            }
          }
        }
      }
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   ROUTE-MIDDLEWARE
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../lib/express');

    var app = express();

    // Example requests:
    //     curl http://localhost:3000/user/0
    //     curl http://localhost:3000/user/0/edit
    //     curl http://localhost:3000/user/1
    //     curl http://localhost:3000/user/1/edit (unauthorized since this is not you)
    //     curl -X DELETE http://localhost:3000/user/0 (unauthorized since you are not an admin)

    // Dummy users
    var users = [
        { id: 0, name: 'tj', email: 'tj@vision-media.ca', role: 'member' }
      , { id: 1, name: 'ciaran', email: 'ciaranj@gmail.com', role: 'member' }
      , { id: 2, name: 'aaron', email: 'aaron.heckmann+github@gmail.com', role: 'admin' }
    ];

    function loadUser(req, res, next) {
      // You would fetch your user from the db
      var user = users[req.params.id];
      if (user) {
        req.user = user;
        next();
      } else {
        next(new Error('Failed to load user ' + req.params.id));
      }
    }

    function andRestrictToSelf(req, res, next) {
      // If our authenticated user is the user we are viewing
      // then everything is fine :)
      if (req.authenticatedUser.id === req.user.id) {
        next();
      } else {
        // You may want to implement specific exceptions
        // such as UnauthorizedError or similar so that you
        // can handle these can be special-cased in an error handler
        // (view ./examples/pages for this)
        next(new Error('Unauthorized'));
      }
    }

    function andRestrictTo(role) {
      return function(req, res, next) {
        if (req.authenticatedUser.role === role) {
          next();
        } else {
          next(new Error('Unauthorized'));
        }
      }
    }

    // Middleware for faux authentication
    // you would of course implement something real,
    // but this illustrates how an authenticated user
    // may interact with middleware

    app.use(function(req, res, next){
      req.authenticatedUser = users[0];
      next();
    });

    app.get('/', function(req, res){
      res.redirect('/user/0');
    });

    app.get('/user/:id', loadUser, function(req, res){
      res.send('Viewing user ' + req.user.name);
    });

    app.get('/user/:id/edit', loadUser, andRestrictToSelf, function(req, res){
      res.send('Editing user ' + req.user.name);
    });

    app.delete('/user/:id', loadUser, andRestrictTo('admin'), function(req, res){
      res.send('Deleted user ' + req.user.name);
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   ROUTE-SEPARATION
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var path = require('path');
    var app = express();
    var logger = require('morgan');
    var cookieParser = require('cookie-parser');
    var methodOverride = require('method-override');
    var site = require('./site');
    var post = require('./post');
    var user = require('./user');

    module.exports = app;

    // Config

    app.set('view engine', 'ejs');
    app.set('views', path.join(__dirname, 'views'));

    /* istanbul ignore next */
    if (!module.parent) {
      app.use(logger('dev'));
    }

    app.use(methodOverride('_method'));
    app.use(cookieParser());
    app.use(express.urlencoded({ extended: true }))
    app.use(express.static(path.join(__dirname, 'public')));

    // General

    app.get('/', site.index);

    // User

    app.get('/users', user.list);
    app.all('/user/:id/:op?', user.load);
    app.get('/user/:id', user.view);
    app.get('/user/:id/view', user.view);
    app.get('/user/:id/edit', user.edit);
    app.put('/user/:id/edit', user.update);

    // Posts

    app.get('/posts', post.list);

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- post.js

    // Fake posts database

    var posts = [
      { title: 'Foo', body: 'some foo bar' },
      { title: 'Foo bar', body: 'more foo bar' },
      { title: 'Foo bar baz', body: 'more foo bar baz' }
    ];

    exports.list = function(req, res){
      res.render('posts', { title: 'Posts', posts: posts });
    };
    
   +---- public
   |      +---- style.css

    body {
      padding: 50px;
      font: 14px "Helvetica Neue", Arial, sans-serif;
    }
    a {
      color: #00AEFF;
      text-decoration: none;
    }
    a.edit {
      color: #000;
      opacity: .3;
    }
    a.edit::before {
      content: ' [';
    }
    a.edit::after {
      content: ']';
    }
    dt {
      font-weight: bold;
    }
    dd {
      margin: 15px;
    }
   +---- site.js

    exports.index = function(req, res){
      res.render('index', { title: 'Route Separation Example' });
    };
    
   +---- user.js

    // Fake user database

    var users = [
      { name: 'TJ', email: 'tj@vision-media.ca' },
      { name: 'Tobi', email: 'tobi@vision-media.ca' }
    ];

    exports.list = function(req, res){
      res.render('users', { title: 'Users', users: users });
    };

    exports.load = function(req, res, next){
      var id = req.params.id;
      req.user = users[id];
      if (req.user) {
        next();
      } else {
        var err = new Error('cannot find user ' + id);
        err.status = 404;
        next(err);
      }
    };

    exports.view = function(req, res){
      res.render('users/view', {
        title: 'Viewing user ' + req.user.name,
        user: req.user
      });
    };

    exports.edit = function(req, res){
      res.render('users/edit', {
        title: 'Editing user ' + req.user.name,
        user: req.user
      });
    };

    exports.update = function(req, res){
      // Normally you would handle all kinds of
      // validation and save back to the db
      var user = req.body.user;
      req.user.name = user.name;
      req.user.email = user.email;
      res.redirect('back');
    };
    
   +---- views
   |      +---- footer.ejs

    </body>
    </html>
    
   |      +---- header.ejs

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title><%= title %></title>
      <link rel="stylesheet" href="/style.css">
    </head>
    <body>
    
   |      +---- index.ejs

    <% include header %>

    <h1><%= title %></h1>

    <ul>
      <li>Visit the <a href="/users">users</a> page.</li>
      <li>Visit the <a href="/posts">posts</a> page.</li>
    </ul>

    <% include footer %>
    
   |      +---- posts
   |            +---- index.ejs

    <% include ../header %>

    <h1>Posts</h1>

    <dl id="posts">
      <% posts.forEach(function(post) { %>
        <dt><%= post.title %></dt>
        <dd><%= post.body %></dd>
      <% }) %>
    </dl>

    <% include ../footer %>
    
   |      +---- users
   |            +---- edit.ejs

    <% include ../header %>

    <h1>Editing <%= user.name %></h1>

    <div id="user">
      <form action="?_method=put", method="post">
        <p>
          Name:
          <input type="text" value="<%= user.name %>" name="user[name]" />
        </p>

        <p>
          Email:
          <input type="email" value="<%= user.email %>" name="user[email]" />
        </p>

        <p>
          <input type="submit" value="Save" />
        </p>
      </form>
    </div>

    <% include ../footer %>
    
   |            +---- index.ejs

    <% include ../header %>

    <h1><%= title %></h1>

    <div id="users">
      <% users.forEach(function(user, index) { %>
        <li>
          <a href="/user/<%= index %>"><%= user.name %></a>
          <a href="/user/<%= index %>/edit">edit</a>
        </li>
      <% }) %>
    </div>

    <% include ../footer %>
    
   |            +---- view.ejs

    <% include ../header %>

    <h1><%= user.name %></h1>

    <div id="user">
      <p>Email: <%= user.email %></p>
    </div>

    <% include ../footer %>
    
   SEARCH
   +---- index.js


    // install redis first:
    // https://redis.io/

    // then:
    // $ npm install redis
    // $ redis-server

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var path = require('path');
    var redis = require('redis');

    var db = redis.createClient();

    // npm install redis

    var app = express();

    app.use(express.static(path.join(__dirname, 'public')));

    // populate search

    db.sadd('ferret', 'tobi');
    db.sadd('ferret', 'loki');
    db.sadd('ferret', 'jane');
    db.sadd('cat', 'manny');
    db.sadd('cat', 'luna');

    /**
    * GET search for :query.
    */

    app.get('/search/:query?', function(req, res){
      var query = req.params.query;
      db.smembers(query, function(err, vals){
        if (err) return res.send(500);
        res.send(vals);
      });
    });

    /**
    * GET client javascript. Here we use sendFile()
    * because serving __dirname with the static() middleware
    * would also mean serving our server "index.js" and the "search.jade"
    * template.
    */

    app.get('/client.js', function(req, res){
      res.sendFile(path.join(__dirname, 'client.js'));
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- public
   |      +---- client.js

    var search = document.querySelector('[type=search]');
    var code = document.querySelector('pre');

    search.addEventListener('keyup', function(){
      var xhr = new XMLHttpRequest;
      xhr.open('GET', '/search/' + search.value, true);
      xhr.onreadystatechange = function(){
        if (xhr.readyState === 4) {
          code.textContent = xhr.responseText;
        }
      };
      xhr.send();
    }, false);
    
   |      +---- index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Search example</title>
      <style type="text/css">
        body {
          font: 14px "Helvetica Neue", Helvetica;
          padding: 50px;
        }
      </style>
    </head>
    <body>
      <h2>Search</h2>
      <p>Try searching for "ferret" or "cat".</p>
      <input type="search" name="search" value="" />
      <pre />
      <script src="/client.js" charset="utf-8"></script>
    </body>
    </html>
    
   SESSION
   +---- index.js


    // install redis first:
    // https://redis.io/

    // then:
    // $ npm install redis
    // $ redis-server

    var express = require('../..');
    var session = require('express-session');

    var app = express();

    // Populates req.session
    app.use(session({
      resave: false, // don't save session if unmodified
      saveUninitialized: false, // don't create session until something stored
      secret: 'keyboard cat'
    }));

    app.get('/', function(req, res){
      var body = '';
      if (req.session.views) {
        ++req.session.views;
      } else {
        req.session.views = 1;
        body += '<p>First time visiting? view this page in several browsers :)</p>';
      }
      res.send(body + '<p>viewed <strong>' + req.session.views + '</strong> times.</p>');
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- redis.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var logger = require('morgan');
    var session = require('express-session');

    // pass the express to the connect redis module
    // allowing it to inherit from session.Store
    var RedisStore = require('connect-redis')(session);

    var app = express();

    app.use(logger('dev'));

    // Populates req.session
    app.use(session({
      resave: false, // don't save session if unmodified
      saveUninitialized: false, // don't create session until something stored
      secret: 'keyboard cat',
      store: new RedisStore
    }));

    app.get('/', function(req, res){
      var body = '';
      if (req.session.views) {
        ++req.session.views;
      } else {
        req.session.views = 1;
        body += '<p>First time visiting? view this page in several browsers :)</p>';
      }
      res.send(body + '<p>viewed <strong>' + req.session.views + '</strong> times.</p>');
    });

    app.listen(3000);
    console.log('Express app started on port 3000');
    
   STATIC-FILES
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var logger = require('morgan');
    var path = require('path');
    var app = express();

    // log requests
    app.use(logger('dev'));

    // express on its own has no notion
    // of a "file". The express.static()
    // middleware checks for a file matching
    // the `req.path` within the directory
    // that you pass it. In this case "GET /js/app.js"
    // will look for "./public/js/app.js".

    app.use(express.static(path.join(__dirname, 'public')));

    // if you wanted to "prefix" you may use
    // the mounting feature of Connect, for example
    // "GET /static/js/app.js" instead of "GET /js/app.js".
    // The mount-path "/static" is simply removed before
    // passing control to the express.static() middleware,
    // thus it serves the file correctly by ignoring "/static"
    app.use('/static', express.static(path.join(__dirname, 'public')));

    // if for some reason you want to serve files from
    // several directories, you can use express.static()
    // multiple times! Here we're passing "./public/css",
    // this will allow "GET /style.css" instead of "GET /css/style.css":
    app.use(express.static(path.join(__dirname, 'public', 'css')));

    app.listen(3000);
    console.log('listening on port 3000');
    console.log('try:');
    console.log('  GET /hello.txt');
    console.log('  GET /js/app.js');
    console.log('  GET /css/style.css');
    
   +---- public
   |      +---- css
   |            +---- style.css

    body {

    }
   |      +---- hello.txt

    hey
   |      +---- js
   |            +---- app.js

    // foo
    
   VHOST
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var logger = require('morgan');
    var vhost = require('vhost');

    /*
    edit /etc/hosts:

    127.0.0.1       foo.example.com
    127.0.0.1       bar.example.com
    127.0.0.1       example.com
    */

    // Main server app

    var main = express();

    if (!module.parent) main.use(logger('dev'));

    main.get('/', function(req, res){
      res.send('Hello from main app!');
    });

    main.get('/:sub', function(req, res){
      res.send('requested ' + req.params.sub);
    });

    // Redirect app

    var redirect = express();

    redirect.use(function(req, res){
      if (!module.parent) console.log(req.vhost);
      res.redirect('http://example.com:3000/' + req.vhost[0]);
    });

    // Vhost app

    var app = module.exports = express();

    app.use(vhost('*.example.com', redirect)); // Serves all subdomains via Redirect app
    app.use(vhost('example.com', main)); // Serves top level domain via Main server app

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   VIEW-CONSTRUCTOR
   +---- github-view.js

    /**
    * Module dependencies.
    */

    var https = require('https');
    var path = require('path');
    var extname = path.extname;

    /**
    * Expose `GithubView`.
    */

    module.exports = GithubView;

    /**
    * Custom view that fetches and renders
    * remove github templates. You could
    * render templates from a database etc.
    */

    function GithubView(name, options){
      this.name = name;
      options = options || {};
      this.engine = options.engines[extname(name)];
      // "root" is the app.set('views') setting, however
      // in your own implementation you could ignore this
      this.path = '/' + options.root + '/master/' + name;
    }

    /**
    * Render the view.
    */

    GithubView.prototype.render = function(options, fn){
      var self = this;
      var opts = {
        host: 'raw.githubusercontent.com',
        port: 443,
        path: this.path,
        method: 'GET'
      };

      https.request(opts, function(res) {
        var buf = '';
        res.setEncoding('utf8');
        res.on('data', function(str){ buf += str });
        res.on('end', function(){
          self.engine(buf, options, fn);
        });
      }).end();
    };
    
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');
    var GithubView = require('./github-view');
    var md = require('marked').parse;

    var app = module.exports = express();

    // register .md as an engine in express view system
    app.engine('md', function(str, options, fn){
      try {
        var html = md(str);
        html = html.replace(/\{([^}]+)\}/g, function(_, name){
          return options[name] || '';
        });
        fn(null, html);
      } catch(err) {
        fn(err);
      }
    });

    // pointing to a particular github repo to load files from it
    app.set('views', 'expressjs/express');

    // register a new view constructor
    app.set('view', GithubView);

    app.get('/', function(req, res){
      // rendering a view relative to the repo.
      // app.locals, res.locals, and locals passed
      // work like they normally would
      res.render('examples/markdown/views/index.md', { title: 'Example' });
    });

    app.get('/Readme.md', function(req, res){
      // rendering a view from https://github.com/expressjs/express/blob/master/Readme.md
      res.render('Readme.md');
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   VIEW-LOCALS
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../..');
    var path = require('path');
    var User = require('./user');
    var app = express();

    app.set('views', path.join(__dirname, 'views'));
    app.set('view engine', 'ejs');

    // filter ferrets only

    function ferrets(user) {
      return user.species === 'ferret'
    }

    // naive nesting approach,
    // delegating errors to next(err)
    // in order to expose the "count"
    // and "users" locals

    app.get('/', function(req, res, next){
      User.count(function(err, count){
        if (err) return next(err);
        User.all(function(err, users){
          if (err) return next(err);
          res.render('index', {
            title: 'Users',
            count: count,
            users: users.filter(ferrets)
          });
        })
      })
    });




    // this approach is cleaner,
    // less nesting and we have
    // the variables available
    // on the request object

    function count(req, res, next) {
      User.count(function(err, count){
        if (err) return next(err);
        req.count = count;
        next();
      })
    }

    function users(req, res, next) {
      User.all(function(err, users){
        if (err) return next(err);
        req.users = users;
        next();
      })
    }

    app.get('/middleware', count, users, function(req, res, next){
      res.render('index', {
        title: 'Users',
        count: req.count,
        users: req.users.filter(ferrets)
      });
    });




    // this approach is much like the last
    // however we're explicitly exposing
    // the locals within each middleware
    //
    // note that this may not always work
    // well, for example here we filter
    // the users in the middleware, which
    // may not be ideal for our application.
    // so in that sense the previous example
    // is more flexible with `req.users`.

    function count2(req, res, next) {
      User.count(function(err, count){
        if (err) return next(err);
        res.locals.count = count;
        next();
      })
    }

    function users2(req, res, next) {
      User.all(function(err, users){
        if (err) return next(err);
        res.locals.users = users.filter(ferrets);
        next();
      })
    }

    app.get('/middleware-locals', count2, users2, function(req, res, next){
      // you can see now how we have much less
      // to pass to res.render(). If we have
      // several routes related to users this
      // can be a great productivity booster
      res.render('index', { title: 'Users' });
    });

    // keep in mind that middleware may be placed anywhere
    // and in various combinations, so if you have locals
    // that you wish to make available to all subsequent
    // middleware/routes you can do something like this:

    /*

    app.use(function(req, res, next){
      res.locals.user = req.user;
      res.locals.sess = req.session;
      next();
    });

    */

    // or suppose you have some /admin
    // "global" local variables:

    /*

    app.use('/api', function(req, res, next){
      res.locals.user = req.user;
      res.locals.sess = req.session;
      next();
    });

    */

    // the following is effectively the same,
    // but uses a route instead:

    /*

    app.all('/api/*', function(req, res, next){
      res.locals.user = req.user;
      res.locals.sess = req.session;
      next();
    });

    */

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    
   +---- user.js

    module.exports = User;

    // faux model

    function User(name, age, species) {
      this.name = name;
      this.age = age;
      this.species = species;
    }

    User.all = function(fn){
      // process.nextTick makes sure this function API
      // behaves in an asynchronous manner, like if it
      // was a real DB query to read all users.
      process.nextTick(function(){
        fn(null, users);
      });
    };

    User.count = function(fn){
      process.nextTick(function(){
        fn(null, users.length);
      });
    };

    // faux database

    var users = [];

    users.push(new User('Tobi', 2, 'ferret'));
    users.push(new User('Loki', 1, 'ferret'));
    users.push(new User('Jane', 6, 'ferret'));
    users.push(new User('Luna', 1, 'cat'));
    users.push(new User('Manny', 1, 'cat'));
    
   +---- views
   |      +---- index.ejs

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title><%= title %></title>
        <style media="screen">
          body {
            padding: 50px;
            font: 16px Helvetica, Arial;
          }
        </style>
      </head>
      <body>
        <h2><%= title %></h2>
        <% users.forEach(function(user) { %>
          <li><strong><%= user.name %></strong> is a <% user.age %> year old <%= user.species %></li>
        <% }); %>
      </body>
    </html>
    
   WEB-SERVICE
   +---- index.js

    /**
    * Module dependencies.
    */

    var express = require('../../');

    var app = module.exports = express();

    // create an error with .status. we
    // can then use the property in our
    // custom error handler (Connect repects this prop as well)

    function error(status, msg) {
      var err = new Error(msg);
      err.status = status;
      return err;
    }

    // if we wanted to supply more than JSON, we could
    // use something similar to the content-negotiation
    // example.

    // here we validate the API key,
    // by mounting this middleware to /api
    // meaning only paths prefixed with "/api"
    // will cause this middleware to be invoked

    app.use('/api', function(req, res, next){
      var key = req.query['api-key'];

      // key isn't present
      if (!key) return next(error(400, 'api key required'));

      // key is invalid
      if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key'));

      // all good, store req.key for route access
      req.key = key;
      next();
    });

    // map of valid api keys, typically mapped to
    // account info with some sort of database like redis.
    // api keys do _not_ serve as authentication, merely to
    // track API usage or help prevent malicious behavior etc.

    var apiKeys = ['foo', 'bar', 'baz'];

    // these two objects will serve as our faux database

    var repos = [
      { name: 'express', url: 'https://github.com/expressjs/express' },
      { name: 'stylus', url: 'https://github.com/learnboost/stylus' },
      { name: 'cluster', url: 'https://github.com/learnboost/cluster' }
    ];

    var users = [
        { name: 'tobi' }
      , { name: 'loki' }
      , { name: 'jane' }
    ];

    var userRepos = {
      tobi: [repos[0], repos[1]]
      , loki: [repos[1]]
      , jane: [repos[2]]
    };

    // we now can assume the api key is valid,
    // and simply expose the data

    // example: http://localhost:3000/api/users/?api-key=foo
    app.get('/api/users', function(req, res, next){
      res.send(users);
    });

    // example: http://localhost:3000/api/repos/?api-key=foo
    app.get('/api/repos', function(req, res, next){
      res.send(repos);
    });

    // example: http://localhost:3000/api/user/tobi/repos/?api-key=foo
    app.get('/api/user/:name/repos', function(req, res, next){
      var name = req.params.name;
      var user = userRepos[name];

      if (user) res.send(user);
      else next();
    });

    // middleware with an arity of 4 are considered
    // error handling middleware. When you next(err)
    // it will be passed through the defined middleware
    // in order, but ONLY those with an arity of 4, ignoring
    // regular middleware.
    app.use(function(err, req, res, next){
      // whatever you want here, feel free to populate
      // properties on `err` to treat it differently in here.
      res.status(err.status || 500);
      res.send({ error: err.message });
    });

    // our custom JSON 404 middleware. Since it's placed last
    // it will be the last middleware called, if all others
    // invoke next() and do not respond.
    app.use(function(req, res){
      res.status(404);
      res.send({ error: "Lame, can't find that" });
    });

    /* istanbul ignore next */
    if (!module.parent) {
      app.listen(3000);
      console.log('Express started on port 3000');
    }
    

Back to Main Page