├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── app.babel.js ├── app.js ├── app ├── config │ ├── config.js │ └── db.js ├── controllers │ ├── index.js │ └── user │ │ ├── index.js │ │ └── user.test.js ├── lib │ └── database.js ├── models │ └── User │ │ ├── index.js │ │ └── validate.js ├── route.js ├── routes │ ├── index.js │ └── users.js ├── services │ └── user.js └── views │ ├── error.pug │ ├── index.pug │ └── layout.pug ├── bin └── www ├── package-lock.json ├── package.json ├── public └── stylesheets │ └── style.css ├── registers.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "es2016", "stage-2"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .jsbeautifyrc 3 | node_modules/ 4 | build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pedram marandi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express+Webpack backend boilerplate 2 | An consistence Express framework with power of the Webpack to design an scalable API backend. 3 | 4 | - Structured config files 5 | - Moongose database 6 | - Structured models 7 | - Structured routes 8 | - Integrated config files with Express config variables 9 | - Structured controllers and services 10 | - Mocha testing 11 | - Build tools and test coverage tools 12 | 13 | ## Installation 14 | 15 | To start developing your application, after you cloned the project do 16 | 17 | ```sh 18 | $ npm install 19 | $ npm run-script run-dev 20 | ``` 21 | --- 22 | 23 | ## Controllers 24 | In order to develope a consistence and scaleable Express application we've defined different conventions. So, all of the controllers should return a promise. huh? hold on, don't freak out it now, it's sweet you will understand why. 25 | 26 | ```javascript 27 | const getUser = async function(username) { 28 | if(username ==='') 29 | throw new Error('Username can\'t be empty'); 30 | 31 | return await getUserByUsername(username); // A service 32 | } 33 | ``` 34 | Also, you may noticed that there isn't any parameter for getUser function in our controller. We'll pass them with a cool trick. It's cool because you've got rid of (req, res, next) in your controllers. 35 | 36 | The logic of our **Controllers** is driven by **Services**. The aim of a **Service** is to developing a reusable code related to **models** and **conrollers**. The returned value of your services could be anything. 37 | 38 | **services/user.js** 39 | ```javascript 40 | export function getUser(username) { 41 | return User.find({username}).exec(); // A promise 42 | } 43 | ``` 44 | So, now we should resolve the Promise which is returned by the controllers in the router. 45 | Here is our router for *getUser* function. 46 | ```javascript 47 | router.get('/:username', async function(req, res, next) { 48 | const data = await userController.getUser(req.params.username); 49 | 50 | return res.json(data); 51 | }); 52 | ``` 53 | As it mentioned before, all of the controllers will return a promise. You should resolve all of the Controller's promises in each router. However, we made a fake route client to do it for you. You can implement this fake router like below. 54 | ```javascript 55 | router.get('/:username', call(userController.getUser, (req, res, next) => [req.params.username])); 56 | ``` 57 | 58 | 59 | ## Models 60 | All of your applications' models goes into ***./app/models*** folder. 61 | 62 | ```javascript 63 | import mongoos from 'mongoose'; 64 | var userSchema = new schema({ 65 | name: { 66 | type: String, 67 | require: true 68 | }, 69 | lastname: { 70 | type: String, 71 | required: true 72 | }, 73 | username: { 74 | type: String, 75 | validator: validateUsername, 76 | msg: "Your username is wrong" 77 | }, 78 | age: { 79 | type: Number, 80 | default: null 81 | }, 82 | password: String 83 | }); 84 | 85 | const User = mongoose.model('User', userSchema); 86 | 87 | export default User; 88 | ``` 89 | You can learn about mongoose from its documentation [Mongoose object mondeling](http://mongoosejs.com/). 90 | 91 | --- 92 | 93 | ## Configs 94 | All of your configs files should be in **./config** folder. 95 | ***./config/main.js*** 96 | ```javascript 97 | configs = Object.assign({ 98 | project: 'ExpressMVC Boilderplate', 99 | url: 'localhost', 100 | port: process.env.PORT || 3000, 101 | api: { 102 | address: 'http://github.com/api' 103 | } 104 | }); 105 | 106 | const environemnts = { 107 | production: { 108 | 109 | }, 110 | development: { 111 | 112 | } 113 | }[process.env.NODE_ENV || 'development']; 114 | 115 | module.exports = Object.assign(project, environemnts); 116 | ``` 117 | Your config file will be accessible throughout your application with the Express ``app.get()`` method. Moreover, if you have nested level config objects, each level will be accessible with a **" . "** 118 | ```javascript 119 | app.get('api.address'); 120 | ``` 121 | 122 | --- 123 | 124 | -------------------------------------------------------------------------------- /app.babel.js: -------------------------------------------------------------------------------- 1 | require('babel-register')({ 2 | presets: ['es2016'] 3 | }); 4 | require('./app'); -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import http from 'http'; 3 | import express from 'express'; 4 | import logger from 'morgan'; 5 | import cookieParser from 'cookie-parser'; 6 | import bodyParser from 'body-parser'; 7 | import { registerConfigs } from './registers'; 8 | import config from './app/config/config'; 9 | import registerRoutes from './app/route'; 10 | 11 | const debug = require('debug')('express:server'); 12 | 13 | const app = express(); 14 | // view engine setup 15 | app.set('views', './app/views'); 16 | app.set('view engine', 'pug'); 17 | 18 | // uncomment after placing your favicon in /public 19 | // app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 20 | app.use(logger('dev')); 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ 23 | extended: false, 24 | })); 25 | app.use(cookieParser()); 26 | app.use(express.static(path.join(__dirname, 'public'))); 27 | 28 | // Register the config file into the app.local 29 | registerConfigs(app, config); // Register configs 30 | registerRoutes(app); // Register routes in application 31 | 32 | const port = normalizePort(process.env.PORT || '3000'); 33 | app.set('port', port); 34 | const server = http.createServer(app); 35 | 36 | server.listen(port, () => { 37 | console.log("I'm listening on port number " + port); 38 | }); 39 | server.on('error', onError); 40 | server.on('listening', onListening); 41 | 42 | function normalizePort(val) { 43 | const port = parseInt(val, 10); 44 | if (isNaN(port)) { 45 | return val; 46 | } 47 | if (port >= 0) { 48 | return port; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | function onError(error) { 55 | if (error.syscall !== 'listen') { 56 | throw error; 57 | } 58 | 59 | const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 60 | 61 | switch (error.code) { 62 | case 'EACCES': 63 | console.error(bind + ' requires elevated privileges'); 64 | process.exit(1); 65 | break; 66 | case 'EADDRINUSE': 67 | console.error(bind + ' is already in use'); 68 | process.exit(1); 69 | break; 70 | default: 71 | throw error; 72 | } 73 | } 74 | 75 | function onListening() { 76 | const addr = server.address(); 77 | const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 78 | debug('Listening on ' + bind); 79 | } 80 | -------------------------------------------------------------------------------- /app/config/config.js: -------------------------------------------------------------------------------- 1 | import database from './db'; 2 | 3 | const project = { 4 | project: 'CRIBBBLE BACKEND', 5 | url: 'localhost', 6 | api: { 7 | url: 'https://api.dribbble.com/v1/', 8 | }, 9 | port: parseInt(process.env.PORT, 10) || 3000, 10 | mongo: database, 11 | }; 12 | 13 | // You can store the environment-based configs in this object 14 | const env = { 15 | production: {}, 16 | development: {}, 17 | }[process.env.NODE_ENV || 'development']; 18 | 19 | const config = Object.assign(project, env); 20 | 21 | module.exports = config; 22 | -------------------------------------------------------------------------------- /app/config/db.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | development: { 3 | adress: 'localhost', 4 | username: 'admin', 5 | password: 'nemigam', 6 | port: 27017, 7 | name: 'marandi', 8 | }, 9 | production: { 10 | adress: 'localhost', 11 | username: '', 12 | password: '', 13 | port: 27017, 14 | name: 'marandi', 15 | }, 16 | test: {}, 17 | }[process.env.NODE_ENV || 'development']; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User'; 2 | 3 | exports.getIndex = () => (req, res) => 4 | res.json({ 5 | status: 'Server is running', 6 | }); 7 | -------------------------------------------------------------------------------- /app/controllers/user/index.js: -------------------------------------------------------------------------------- 1 | import {getUser as getUserByUsername} from '../../services/user'; 2 | import {createUser} from '../../services/user'; 3 | 4 | /** 5 | * Get a user by username. 6 | * @param username a string value that represents user's username. 7 | * @returns A Promise, an exception or a value. It depends on the service or controller treatment. 8 | */ 9 | exports.getUser = async function(username) { 10 | if(username ==='') 11 | throw new Error('Username can\'t be empty'); 12 | 13 | return await getUserByUsername(username); 14 | } 15 | 16 | exports.create = async function (params) { 17 | if(params.username === '') 18 | return new Error('Username can\'t be empty'); 19 | 20 | return await createUser(params); 21 | } -------------------------------------------------------------------------------- /app/controllers/user/user.test.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import assert from 'assert'; 3 | 4 | describe('Array', function() { 5 | describe('#indexOf()', function() { 6 | it('should return -1 when the value is not present', function() { 7 | assert.equal(-1, [1,2,3].indexOf(4)); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/lib/database.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { mongo } from '../config/config'; 3 | 4 | const options = { 5 | useMongoClient: true, 6 | }; 7 | const databaseURI = `mongodb://${mongo.username}:${mongo.password}@${mongo.adress}:${mongo.port}/${ 8 | mongo.name 9 | }?authSource=admin`; 10 | 11 | mongoose.Promise = global.Promise; 12 | mongoose 13 | .connect(databaseURI, options) 14 | .then((db) => { 15 | console.log(`~DATABASE~Express Server has Connected to the "${db.db.s.databaseName}" Database`); 16 | }) 17 | .catch((e) => { 18 | console.log('~DATABASE~ Something is wrong' + e); 19 | }); 20 | -------------------------------------------------------------------------------- /app/models/User/index.js: -------------------------------------------------------------------------------- 1 | import mongoose, { 2 | Schema as schema 3 | } from 'mongoose'; 4 | import validateUsername from './validate'; 5 | var userSchema = new schema({ 6 | name: { 7 | type: String, 8 | require: true 9 | }, 10 | lastname: { 11 | type: String, 12 | required: true 13 | }, 14 | username: { 15 | type: String, 16 | validator: validateUsername, 17 | msg: "Your username is wrong" 18 | }, 19 | age: { 20 | type: Number, 21 | default: null 22 | }, 23 | password: String 24 | }); 25 | 26 | const User = mongoose.model('User', userSchema); 27 | 28 | export default User; -------------------------------------------------------------------------------- /app/models/User/validate.js: -------------------------------------------------------------------------------- 1 | export function validateUsername(username) { 2 | return true; 3 | } -------------------------------------------------------------------------------- /app/route.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import index from './routes/index'; 3 | import users from './routes/users'; 4 | 5 | function register(app) { 6 | app.use('/', index); 7 | app.use('/users', users); 8 | 9 | 10 | // catch 404 and forward to error handler 11 | app.use(function(req, res, next) { 12 | var err = new Error('Not Found'); 13 | err.status = 404; 14 | next(err); 15 | }); 16 | 17 | // error handler 18 | app.use(function(err, req, res, next) { 19 | // set locals, only providing error in development 20 | res.locals.message = err.message; 21 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 22 | 23 | // render the error page 24 | res.status(err.status || 500); 25 | res.render('error'); 26 | }); 27 | }; 28 | 29 | export default register; -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import Index from '../controllers/index'; 3 | 4 | const router = Router(); 5 | router.get('/', Index.getIndex()); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /app/routes/users.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | const router = Router(); 4 | /* GET users listing. */ 5 | router.get('users/:username', (req, res) => { 6 | const { username } = req.params; 7 | return res.json({ 8 | username, 9 | status: 'User is activated', 10 | }); 11 | }); 12 | 13 | module.exports = Router; 14 | -------------------------------------------------------------------------------- /app/services/user.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User'; 2 | 3 | export function getUser(username) { 4 | return User.find({username}).exec(); // A promise 5 | } 6 | 7 | export function createUser(params) { 8 | const {username, firstname, lastname, password} = params; 9 | 10 | return User.create({ 11 | username, 12 | firstname, 13 | lastname, 14 | password 15 | }); 16 | } 17 | 18 | export function isPedram(firstname) { 19 | return firstname === 'Pedram'; 20 | } 21 | -------------------------------------------------------------------------------- /app/views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} -------------------------------------------------------------------------------- /app/views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /app/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var debug = require('debug')('express:server'); 7 | var http = require('http'); 8 | 9 | /** 10 | * Get port from environment and store in Express. 11 | */ 12 | const env = process.env.NODE_ENV; 13 | if(env == 'production') { 14 | var app = require('../build/backend'); 15 | } else { 16 | var app = require('../app'); 17 | 18 | } 19 | 20 | var port = normalizePort(process.env.PORT || '3000'); 21 | app.set('port', port); 22 | 23 | /** 24 | * Create HTTP server. 25 | */ 26 | 27 | var server = http.createServer(app); 28 | 29 | /** 30 | * Listen on provided port, on all network interfaces. 31 | */ 32 | 33 | server.listen(port, () => { 34 | console.log("I'm listening on port number " + port); 35 | }); 36 | server.on('error', onError); 37 | server.on('listening', onListening); 38 | 39 | /** 40 | * Normalize a port into a number, string, or false. 41 | */ 42 | 43 | function normalizePort(val) { 44 | var port = parseInt(val, 10); 45 | 46 | if (isNaN(port)) { 47 | // named pipe 48 | return val; 49 | } 50 | 51 | if (port >= 0) { 52 | // port number 53 | return port; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | /** 60 | * Event listener for HTTP server "error" event. 61 | */ 62 | 63 | function onError(error) { 64 | if (error.syscall !== 'listen') { 65 | throw error; 66 | } 67 | 68 | var bind = typeof port === 'string' 69 | ? 'Pipe ' + port 70 | : 'Port ' + port; 71 | 72 | // handle specific listen errors with friendly messages 73 | switch (error.code) { 74 | case 'EACCES': 75 | console.error(bind + ' requires elevated privileges'); 76 | process.exit(1); 77 | break; 78 | case 'EADDRINUSE': 79 | console.error(bind + ' is already in use'); 80 | process.exit(1); 81 | break; 82 | default: 83 | throw error; 84 | } 85 | } 86 | 87 | /** 88 | * Event listener for HTTP server "listening" event. 89 | */ 90 | 91 | function onListening() { 92 | var addr = server.address(); 93 | var bind = typeof addr === 'string' 94 | ? 'pipe ' + addr 95 | : 'port ' + addr.port; 96 | debug('Listening on ' + bind); 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expressmvc", 3 | "version": "1.0.0", 4 | "description": "Webpack and Express backend boilerplate Edit", 5 | "main": "app.babel.js", 6 | "dependencies": { 7 | "babel-runtime": "^6.23.0", 8 | "body-parser": "~1.16.0", 9 | "cookie-parser": "~1.4.3", 10 | "debug": "~2.6.0", 11 | "express": "~4.14.1", 12 | "mongoose": "^4.9.2", 13 | "morgan": "~1.7.0", 14 | "nodemon": "^1.11.0", 15 | "pug": "~2.0.0-beta10", 16 | "serve-favicon": "~2.3.2" 17 | }, 18 | "devDependencies": { 19 | "babel-cli": "^6.24.0", 20 | "babel-core": "^6.26.0", 21 | "babel-loader": "^7.1.2", 22 | "babel-polyfill": "^6.26.0", 23 | "babel-preset-es2015": "^6.24.0", 24 | "babel-preset-es2016": "^6.22.0", 25 | "babel-preset-stage-2": "^6.24.1", 26 | "babel-register": "^6.26.0", 27 | "mocha": "^3.5.0", 28 | "webpack": "^3.5.5" 29 | }, 30 | "scripts": { 31 | "test": "mocha 'app/**/*.test.js' --compilers js:babel-core/register --recursive", 32 | "start": "NODE_ENV=production node ./build/backend.js", 33 | "dev": "NODE_ENV=development DEBUG=express:* nodemon ./app.js --exec babel-node", 34 | "build": 35 | "rm -r build && NODE_ENV=production webpack --config ./webpack.config.js --progress --profile --colors" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/PedramMarandi/express-js-boilerplate.git" 40 | }, 41 | "keywords": ["expressjs", "boilerplate", "es6", "javascript", "MVC"], 42 | "author": "Pedram Marandi", 43 | "license": "ISC", 44 | "bugs": { 45 | "url": "https://github.com/PedramMarandi/express-js-boilerplate/issues" 46 | }, 47 | "homepage": "https://github.com/PedramMarandi/express-js-boilerplate#readme" 48 | } 49 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /registers.js: -------------------------------------------------------------------------------- 1 | export function registerConfigs(app, config, name = '') { 2 | let configKey; 3 | let itemCount = 0; 4 | name.length > 0 ? configKey = name : configKey = ''; 5 | 6 | for (let item in config) { 7 | if (typeof config[item] == 'object') { 8 | itemCount = Object.keys(config[item]).length; 9 | configKey.length == 0 ? configKey = `${item}.` : configKey += `${item}.`; 10 | 11 | registerConfigs(app, config[item], configKey); 12 | } else { 13 | itemCount--; 14 | let keyName = configKey; 15 | 16 | configKey.length == 0 ? keyName = item : keyName += item; 17 | app.set(keyName, config[item]); 18 | itemCount == 0 ? configKey = '' : configKey; 19 | } 20 | } 21 | } 22 | 23 | export function registerLocal(app, locals) { 24 | app.locals = { ...app.locals, 25 | ...locals 26 | }; 27 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | 5 | 6 | var nodeModules = {}; 7 | fs.readdirSync('node_modules') 8 | .filter(function(x) { 9 | return ['.bin'].indexOf(x) === -1; 10 | }) 11 | .forEach(function(mod) { 12 | nodeModules[mod] = 'commonjs ' + mod; 13 | }); 14 | 15 | 16 | 17 | module.exports = { 18 | entry: ['babel-polyfill', './app.js'], 19 | target: 'node', 20 | output: { 21 | path: path.join(__dirname, 'build'), 22 | filename: 'backend.js' 23 | }, 24 | externals: nodeModules, 25 | module: { 26 | rules: [{ 27 | test: /\.js$/, 28 | exclude: /(node_modules|bower_components)/, 29 | use: { 30 | loader: 'babel-loader', 31 | options: { 32 | presets: ['env'], 33 | plugins: [require('babel-plugin-transform-object-rest-spread')] 34 | } 35 | } 36 | }] 37 | } 38 | } --------------------------------------------------------------------------------