├── .gitignore ├── README.md ├── app.js ├── bin └── www ├── package.json ├── public └── stylesheets │ └── style.sass ├── routes ├── index.js └── users.js └── views ├── error.twig ├── index.twig └── layout.twig /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/cli/shrinkwrap#caveats 27 | node_modules 28 | 29 | # Debug log from npm 30 | npm-debug.log 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twelve Factor nodejs app 2 | 3 | _This is a work in progress._ 4 | 5 | This app demonstrates the [twelve-factor methodology](https://12factor.net) in 6 | Node.js on cloud.gov. 7 | 8 | The goal is to walk a user through the twelve-factor methodology with specific 9 | examples of how you would implement them in Node.js on cloud.gov. The companion 10 | application that demonstrates the principles is not implemented yet, but the 11 | idea is that you could link to specific line numbers to see a working app, with 12 | working code implementing best practices. 13 | 14 | The twelve-factor methodology is not specific to Node.js and much of these tips 15 | are already general enough for any cloud.gov application. 16 | 17 | 18 | ## The Twelve Factors 19 | 20 | 21 | ### [I. Codebase](https://12factor.net/codebase) 22 | 23 | One codebase tracked in revision control, many deploys. 24 | 25 | 26 | #### How we do it 27 | 28 | This code is tracked on github. [Git 29 | flow](https://danielkummer.github.io/git-flow-cheatsheet/) can be used to manage 30 | branches for releases. 31 | 32 | [comment]: # (include notes on the CD scripts) 33 | 34 | 35 | ### [II. Dependencies](https://12factor.net/dependencies) 36 | 37 | Explicitly declare and isolate dependencies. 38 | 39 | 40 | #### How we do it 41 | 42 | [package.json][package-json] declare and lock dependencies to specific versions. 43 | [npm][npmjs] installs modules to a local `node_modules` dir so each 44 | application's dependencies are isolated from the rest of the system. 45 | 46 | 47 | ### [III. Config](https://12factor.net/config) 48 | 49 | Store config in the environment. 50 | 51 | 52 | #### How we do it 53 | 54 | Configuration is stored in enviornment variables and supplied through the 55 | [manifest.yml][manifest-yml]. 56 | 57 | Secrets are also stored in environment variables but supplied through a [Cloud 58 | Foundry User Provided 59 | Service](https://docs.cloudfoundry.org/devguide/services/user-provided.html). 60 | When setting up the app, they are created with a one-time command `cf 61 | create-user-provided-service tfn-secrets -p '{"SECRET_KEY": 62 | "your-secret-key"}'`. 63 | 64 | Connection configuration to Cloud Foundry Services, like our database, are 65 | provided through the `VCAP_SERVICES` environment variable. 66 | 67 | 68 | ### [IV. Backing services](https://12factor.net/backing-services) 69 | 70 | Treat backing services as attached resources. 71 | 72 | 73 | #### How we do it 74 | 75 | We connect to the database via a connection url provided by the 76 | `VCAP_SERVICES` environment variable. If we needed to setup a new database, we 77 | would simply create a new database with `cf create-service` and bind the 78 | database to our application. After restaging with `cf restage`, the 79 | `VCAP_SERVICES` environment will be updated with the new connection url and our 80 | app would be talking to the new database. 81 | 82 | We use a library which handles the database connection. This library abstracts 83 | away the differences between different SQL-based databases. This makes it easier 84 | to migrate from one database provider to another. 85 | 86 | We expect to be using a database hosted on Cloud Foundry, but using this 87 | strategy we could store the connection url in a separate environment variable 88 | which could point to a database outside of the Cloud Foundry environment and 89 | this strategy would work fine. 90 | 91 | Of course, how you handle migrating your data from one database to another can 92 | be complicated and is out of scope with regard to the twelve factor app. 93 | 94 | 95 | ### [V. Build, release, run](https://12factor.net/build-release-run) 96 | 97 | Strictly separate build and run stages. 98 | 99 | 100 | #### How we do it 101 | 102 | `package.json` allows to configure "scripts" so that we can codify various 103 | tasks. `npm run build` is used to build this application and produces minified 104 | javascript and css files to be served as static assets. 105 | 106 | `npm start` is used to start the application. The `nodejs_buildpack` runs this 107 | command by default to start your application. 108 | 109 | 110 | ### [VI. Processes](https://12factor.net/processes) 111 | 112 | Execute the app as one or more stateless processes. 113 | 114 | 115 | #### How we do it 116 | 117 | We listen to SIGTERM and SIGINT to know it's time to shutdown. The platform is 118 | constantly being updated even if our application is not. Machines die, security 119 | patches cause reboots. Server resources become consumed. Any of these things 120 | could cause the platform to kill your application. Don't worry though, Cloud 121 | Foundry makes sure to start a new process on the new freshly patched host before 122 | killing your old process. 123 | 124 | By listening to process signals, we know when to stop serving requests, flush 125 | database connections, and close any open resources. 126 | 127 | 128 | ### [VII. Port binding](https://12factor.net/port-binding) 129 | 130 | Export services via port binding. 131 | 132 | 133 | #### How we do it 134 | 135 | Cloud Foundry assigns your application instance a port on the host machine and 136 | exposes it through the `PORT` environment variable. 137 | 138 | 139 | ### [VIII. Concurrency](https://12factor.net/concurrency) 140 | 141 | Scale out via the process model 142 | 143 | 144 | #### How we do it 145 | 146 | Our app keeps no state on it's own. Configuration is stored in the environment 147 | and read at startup. User sessions are stored as cookies on the client. Any 148 | other state is kept in the database. This allows our application to scale simply 149 | by adding more processes. 150 | 151 | We are running [two application instances][manifest-yml-instances] on Cloud 152 | Foundry. Each application instance represents a running process of our 153 | application. The two instances are likely running on different host machines and 154 | have no way of communicating with each other. By making our processes stateless, 155 | the two application instances have no need to communicate because all state is 156 | stored in our backing service (the database in our case). 157 | 158 | Here are the steps the app takes when a request comes in: 159 | 160 | 1. A user request comes in. 161 | 1. We parse their session cookie to figure out who they are. 162 | 1. We lookup their user information in the database. 163 | 1. Process their request, possibly with additional database lookups. 164 | 165 | Notice how if a user's request comes in on instance 1, the same user's second 166 | request could be served by any instance. The steps to process subsequent requests are 167 | the same. 168 | 169 | If you wanted to add some kind of session caching, that would be a job for 170 | another backing service like Memcached or Redis. That way all instances of your 171 | application could use a shared cache. 172 | 173 | 174 | ### [IX. Disposability](https://12factor.net/disposability) 175 | 176 | Maximize robustness with fast startup and graceful shutdown. 177 | 178 | 179 | #### How we do it 180 | 181 | _SIGTERM SIGINT above?_ 182 | 183 | ### [X. Dev/prod parity](https://12factor.net/dev-prod-parity) 184 | 185 | Keep development, staging, and production as similar as possible 186 | 187 | #### How we do it 188 | 189 | Choosing your environments is up to you, but it's probably good to have at least 190 | two for development. For us, development is our local laptop. Staging is a Cloud 191 | Foundry environment we use to preview the application to our partners. 192 | Production is a Cloud Foundry environment once it has been accepted by our 193 | partners. 194 | 195 | As much as possible, the differences between development, staging, and 196 | production is simply the configuration which is stored in the environment. 197 | 198 | Occasionally we use `NODE_ENV`, `NODE_CONFIG` to produce slightly different 199 | behavior. Specifically, anything we 200 | 201 | | Environment variable | Description | 202 | | -------------------- | ----------- | 203 | | `NODE_ENV` | _How_ the app is running. | 204 | | `NODE_CONFIG` | _Where_ the app is running. | 205 | 206 | 207 | | Environment variable | development | staging | production | 208 | | -------------------- | ----------- | ------- | ---------- | 209 | | `NODE_ENV` | `` | `production`† | `production` | 210 | | `NODE_CONFIG` | `` | `staging` | `production` | 211 | 212 | *† That's not a typo, remeber `NODE_ENV` is _how_ the app is running. Both 213 | staging and production are Cloud Foundry environments and warrant a production setup.* 214 | 215 | For nodejs, `NODE_ENV=production` has special meaning. `npm install` will only 216 | install `dependencies` listed in your `package.json` and will omit any 217 | `devDependencies`. We also use `NODE_ENV` to condition on how we build our 218 | static assets. `NODE_ENV=production` will include some extra optimizations. 219 | 220 | `NODE_CONFIG` is used sparingly, and only to load environment specific 221 | configuration files. 222 | 223 | 224 | ### [XI. Logs](https://12factor.net/logs) 225 | 226 | Treat logs as event streams. 227 | 228 | 229 | #### How we do it 230 | 231 | We use `winston` as our logger. We use logging levels to provide feedback about 232 | how the application is working. Some of this feedback could warrant a bug fix. 233 | 234 | Warnings are conditions that are unexpected and might hint that a bug exists in 235 | the code. 236 | 237 | 238 | ### [XII. Admin processes](https://12factor.net/admin-processes) 239 | 240 | Run admin/management tasks as one-off processes. 241 | 242 | 243 | #### How we do it 244 | 245 | Any one-off tasks are added as npm scripts. The meat of these tasks is added to 246 | the `tasks` directory. Some take inputs which can be specified when running the 247 | task `npm run script -- arguments`. Note that by default, we avoid writing 248 | interactive scripts. If configuration is complex, the task can accept 249 | a configuration file or read a configuration from stdin. 250 | 251 | 252 | ## Beyond Twelve Factor 253 | 254 | ### Blue/green deploy 255 | 256 | By default, `cf push` will stop your application while it rebuilds the new one. 257 | That means that a `cf push` results in a service disruption. If your application 258 | fails to build, the old application is gone so you are left with no running 259 | application at all. Blue/green is a strategy that creates a new application 260 | living side-by-side your old application. When the new application is known to 261 | be good, the old application is removed and you are left with a working new 262 | version of your application. 263 | 264 | The [autopilot zero-downtime-push](https://github.com/contraband/autopilot) 265 | plugin will do this for you automatically. 266 | 267 | 268 | ### Continuous Integration and Delivery with Git Flow 269 | 270 | Git Flow is a useful process for managing relases using git branches. The idea 271 | is that branches map to different deployment environments. 272 | 273 | `master` represents well-vetted and partner accepted work and deploys to the 274 | production environment. 275 | 276 | `release-x.y` is on track for release and deploys to your staging environment. 277 | This allows partners to review the work before it is released. Each release 278 | branch is based on development. Once created, this allows developers to commit 279 | bug fixes directly to the release branch without hindering development on the 280 | next release. 281 | 282 | `development` is an integration branch. This allows developers on your team 283 | to test their work-in-progress features with features from other developers. 284 | 285 | `feature-\*` branches represent a single feature. One or more developers may be 286 | committing to this branch. When the feature is tested and reviewed it can be 287 | merged to `development`. 288 | 289 | 290 | 291 | [manifest-yml]: https://github.com/adborden/twelve-factor-node/blob/master/manifest.prod.yml 292 | [manifest-yml-instances]: https://github.com/adborden/twelve-factor-node/blob/master/manifest.prod.yml#L3 293 | [npmjs]: https://npmjs.org/ 294 | [package-json]: https://github.com/adborden/twelve-factor-node/blob/master/package.json 295 | [package-json-scripts]: https://github.com/adborden/twelve-factor-node/blob/master/package.json#L5 296 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var sassMiddleware = require('node-sass-middleware'); 8 | 9 | var index = require('./routes/index'); 10 | var users = require('./routes/users'); 11 | 12 | var app = express(); 13 | 14 | // view engine setup 15 | app.set('views', path.join(__dirname, 'views')); 16 | app.set('view engine', 'twig'); 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({ extended: false })); 23 | app.use(cookieParser()); 24 | app.use(sassMiddleware({ 25 | src: path.join(__dirname, 'public'), 26 | dest: path.join(__dirname, 'public'), 27 | indentedSyntax: true, // true = .sass and false = .scss 28 | sourceMap: true 29 | })); 30 | app.use(express.static(path.join(__dirname, 'public'))); 31 | 32 | app.use('/', index); 33 | app.use('/users', users); 34 | 35 | // catch 404 and forward to error handler 36 | app.use(function(req, res, next) { 37 | var err = new Error('Not Found'); 38 | err.status = 404; 39 | next(err); 40 | }); 41 | 42 | // error handler 43 | app.use(function(err, req, res, next) { 44 | // set locals, only providing error in development 45 | res.locals.message = err.message; 46 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 47 | 48 | // render the error page 49 | res.status(err.status || 500); 50 | res.render('error'); 51 | }); 52 | 53 | module.exports = app; 54 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('shopping-12:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopping-12", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.17.1", 10 | "cookie-parser": "~1.4.3", 11 | "debug": "~2.6.3", 12 | "express": "~4.15.2", 13 | "morgan": "~1.8.1", 14 | "serve-favicon": "~2.4.2", 15 | "twig": "~0.10.3" 16 | }, 17 | "devDependencies": { 18 | "node-sass": "^4.5.2", 19 | "node-sass-middleware": "0.9.8" 20 | }, 21 | "engines": { 22 | "node": "6.10.x", 23 | "npm": "3.x.x" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/stylesheets/style.sass: -------------------------------------------------------------------------------- 1 | body 2 | padding: 50px 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif 4 | 5 | a 6 | color: #00B7FF 7 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET users listing. */ 5 | router.get('/', function(req, res, next) { 6 | res.send('respond with a resource'); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /views/error.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block body %} 4 |

{{message}}

5 |

{{error.status}}

6 |
{{error.stack}}
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /views/index.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block body %} 4 |

{{title}}

5 |

Welcome to {{title}}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /views/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | {% block body %}{% endblock %} 9 | 10 | 11 | --------------------------------------------------------------------------------