├── .gitignore ├── README.md ├── architecture.png ├── package.json └── src ├── app.js ├── core └── user-core.js ├── endpoints ├── endpoint-utils.js └── users-endpoint.js ├── index.js └── router.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | # Node 4 | 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | # SASS 33 | .sass-cache 34 | *.css.map 35 | 36 | 37 | # OS X 38 | .DS_Store 39 | 40 | # IDEA editor 41 | .idea 42 | 43 | # Secrets as environment variables 44 | secrets.sh 45 | 46 | # Local dev bundle 47 | local-dev/bundle.js 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express example 2 | 3 | **This example will not run! Some parts are not implemented.** 4 | 5 | This API specific Express app example is demonstrating 6 | in my opinion a good architecture. 7 | 8 | You can also read these slides for presentation called "Good Express.js Architecture": 9 | 10 | * [PDF with speaker notes](https://www.dropbox.com/s/hi8vkndliwiijxf/express-tips-speaker-notes.pdf?dl=0) 11 | * [PDF without speaker notes](https://www.dropbox.com/s/q2iyzx22m96ctqf/express-tips.pdf?dl=0) 12 | 13 | *Note: the terminology in this exampls is a bit different than in the slides. 14 | `controller = endpoint = http-layer` and `service = core = business logic`.* 15 | 16 | Here's how the Express API application has been architectured in the example: 17 | 18 | ![architecture](architecture.png) 19 | 20 | Good with this setup: 21 | 22 | * Your code can leverage Promises 23 | * Testable 24 | 25 | * Express app instance can be created with a function 26 | * Router instance can be created with a function 27 | * Core methods are testable. This would allow you to write some lower level tests. 28 | 29 | Note: even though they are testable, usually you get the best ROI by testing 30 | with real HTTP API calls. In other words: put your express app running locally, init db to 31 | certain state, and fire http requests to localhost with e.g. supertest lib. 32 | 33 | * Provides a minimal but nice "frame" around your development with separation of concerns 34 | * Error handling is done in one place, in the error-handling middlewares defined in app.js 35 | * Your request handling code can throw errors and they will be handled at the app level 36 | 37 | No need to repeat the `next(err)` pattern in all your 38 | request handlers. 39 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimmobrunfeldt/express-example/09e11619169eb5b1c49783213ee8c0aa8ca9e913/architecture.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-error-handling-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "express": "^4.13.3", 14 | "lodash": "^4.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var express = require('express'); 3 | var createRouter = require('./router'); 4 | 5 | // We are taking some inputs directly out from process.env. 6 | // One improvement here would be so that createApp would take all inputs 7 | // as parameters so that the only entrypoint for any configuration to the 8 | // express app would be through the parameters. 9 | function createApp() { 10 | const app = express(); 11 | 12 | // TODO: add express middlewares such as body parser etc here. 13 | 14 | // Initialize routes 15 | const router = createRouter(); 16 | app.use('/api', router); 17 | 18 | app.use(function errorLogger(err, req, res, next) { 19 | const status = err.status ? err.status : 500; 20 | 21 | if (status >= 400) { 22 | console.error('Request headers:'); 23 | console.error(JSON.stringify(req.headers)); 24 | console.error('Request parameters:'); 25 | console.error(JSON.stringify(req.params)); 26 | } 27 | 28 | if (process.env.NODE_ENV === 'test' && status >= 500 || 29 | process.env.NODE_ENV === 'development' 30 | ) { 31 | console.log(err.stack); 32 | } 33 | 34 | next(err); 35 | }); 36 | 37 | app.use(function errorResponder(err, req, res, next) { 38 | const status = err.status ? err.status : 500; 39 | const httpMessage = http.STATUS_CODES[status]; 40 | 41 | let message; 42 | if (status < 500) { 43 | message = httpMessage + ': ' + err.message; 44 | } else { 45 | message = httpMessage; 46 | } 47 | 48 | let response = {message: message}; 49 | if (err.data) { 50 | response.errors = err.data; 51 | } 52 | 53 | res.status(status); 54 | res.send(response); 55 | }); 56 | 57 | // Why a createApp function? 58 | // Implementing tests are much easier when you can create a new instance 59 | // of the express app at any point. 60 | return app; 61 | } 62 | 63 | module.exports = createApp 64 | -------------------------------------------------------------------------------- /src/core/user-core.js: -------------------------------------------------------------------------------- 1 | // TODO: Add a knex instance and configuration for it 2 | var _ = require('lodash'); 3 | var knex = require('./our-configured-knex-instance'); 4 | 5 | // All core methods return promises so that interacting with and chaining them 6 | // is easier. 7 | // You might need to use the same core function from multiple places in the code 8 | // base. 9 | // Consider these core methods as "generic" providers to read/write 10 | // to your database. They can be used even though the request came via HTTP, 11 | // websocket, socket or any other mechanism. 12 | // They might be used from a worker task etc. 13 | // Core function takes precise and minimal input to get the action done. 14 | // You're doing something wrong if you pass e.g. express' `req` object here. 15 | // Core functions should return JSON serializable JS objects. 16 | // If you are using e.g. Bookshelf models, you should not reveal the model 17 | // details outside to the other layers of code. You should keep these functions 18 | // clean in that sense. 19 | function getUserById(userId) { 20 | return knex 21 | .select('*') 22 | .from('users') 23 | .where({ 24 | // NOTE: userId is UNTRUSTED input, you should ALWAYS 25 | // use proper methods to prevent SQL injections. 26 | // In this case knex will take care of that when we use the where 27 | // function. With knex.raw, you should use ? escaping. 28 | id: userId 29 | }) 30 | .limit(1) 31 | .then(rows => { 32 | if (_.isEmpty(rows)) { 33 | return null; 34 | } 35 | 36 | return _userRowToUserObject(rows[0]); 37 | }); 38 | } 39 | 40 | function _userRowToUserObject(row) { 41 | // This conversion could be also separated to a more generic util function 42 | // which automatically converts snake_case to -> camelCase etc. 43 | return { 44 | id: row.id, 45 | name: row.name, 46 | // Convert db convention to js camelcase 47 | fullAddress: row['full_address'] 48 | }; 49 | } 50 | 51 | module.exports = { 52 | getUserById: getUserById 53 | }; 54 | -------------------------------------------------------------------------------- /src/endpoints/endpoint-utils.js: -------------------------------------------------------------------------------- 1 | // This function eliminates boilerplate code in your express request handlers 2 | // when using Promises in your own code. 3 | // This also makes sure that you won't forget to call next with an error. 4 | // `func` is your custom handler code and should always return a promise 5 | function createJsonRoute(func) { 6 | return function route(req, res, next) { 7 | try { 8 | func(req, res) 9 | .then(result => res.json(result)) 10 | .catch(next); 11 | } catch (err) { 12 | next(err); 13 | } 14 | } 15 | } 16 | 17 | module.exports = { 18 | createJsonRoute: createJsonRoute 19 | }; 20 | -------------------------------------------------------------------------------- /src/endpoints/users-endpoint.js: -------------------------------------------------------------------------------- 1 | var epUtils = require('./endpoint-utils'); 2 | var userCore = require('../core/user-core'); 3 | 4 | var getUser = epUtils.createJsonRoute((req, res) => { 5 | // "Unwrap" the HTTP request 6 | // Take all needed input parameters from the request, and pass it 7 | // to lower level core, which shouldn't know about HTTP in general. 8 | var userId = req.params.userId; 9 | 10 | // Validate that the input parameters of the HTTP request "look" correct 11 | // E.g. is it a number between 1-100? 12 | if (isNan(userId)) { 13 | var err = new Error('User id parameter should be a number'); 14 | err.status = 400; 15 | throw err; 16 | } 17 | 18 | return userCore.getUserById(userId) 19 | .then(user => { 20 | if (!user) { 21 | var err = new Error('User does not exist.'); 22 | err.status = 404; 23 | throw err; 24 | } 25 | 26 | return user; 27 | }); 28 | }); 29 | 30 | module.exports = { 31 | getUser: getUser 32 | }; 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var createApp = require('./app'); 2 | 3 | var server = app.listen(process.env.PORT, () => { 4 | logger.info( 5 | 'Express server listening on port %d in %s mode', 6 | process.env.PORT, 7 | app.get('env') 8 | ); 9 | }); 10 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var usersEndpoint = require('./users-endpoint'); 3 | 4 | function createRouter() { 5 | const router = express.Router(); 6 | 7 | router.get('/users/:userId', usersEndpoint.getUser); 8 | 9 | // Usually I make an endpoint file per REST resource 10 | // For example notes resource would be a new file: notes-endpoint.js 11 | // router.get('/notes/:noteId', notesEndpoint.getNote); 12 | 13 | return router; 14 | } 15 | 16 | module.exports = createRouter; 17 | --------------------------------------------------------------------------------