Manage Node.js dependencies properly

Author of every piece of software bigger than simple “Hello World” sooner or later will face problem with dependency management. In this post I would like to consider that problem and possible solutions in regard to Node.js applications.
In Node.js world managing dependencies with npm is exceptionally simple, however there are some caveats which everyone should be aware of.

Have a look at example package.json file:


{  
  "name": "",  
  "description": "",  
  "version": "0.0.1",  
  "private": true,  
  "dependencies": {    
    "express": "~4.2.x",    
    "compression": "~1.0.x",    
    "serve-favicon": "~2.0.x",    
    "morgan": "~1.0.x",    
    "cookie-parser": "~1.1.x",    
    "body-parser": "~1.2.x",    
    "method-override": "~1.0.x",    
    "express-session": "~1.1.x",    
    "serve-static": "~1.1.x",    
    "connect-redis": "~2.0.x",    
    "redis": "~0.10.2",    
    "open": "~0.0.x",    
    "request": "~2.34.x",    
    "connect": "~2.14.x",    
    "connect-flash": "~0.1.x",    
    "crypto": "~0.0.x",    
    "passport": "~0.2.x",    
    "passport-local": "~1.0.x",    
    "underscore": "~1.6.x",    
    "async": "~0.6.x",    
    "moment": "~2.5.x",    
    "ejs": "~1.0.x",    
    "cookie": "~0.1.x",    
    "winston": "~0.7.x",    
    "path": "~0.4.x",    
    "stompjs": "~2.3.x",    
    "socket.io": "~0.9.x",    
    "forever-monitor": "~1.2.x",  
  },  

  "devDependencies": {    
    "supertest": "latest",    
    "should": "latest",    
    "karma": "latest",    
    "karma-junit-reporter": "latest",    
    "karma-ng-html2js-preprocessor": "latest",    
    "grunt": "latest",    
    "grunt-contrib-jshint": "latest",    
    "jshint-junit-reporter": "latest",    
    "grunt-devperf": "latest"  
    }
  }

Every package has a version number which comply with what is described as semantic versioning. Generally speaking, it takes the form of major.minor.patch where major, minor and patch are integers increasing after each new release. Having package.json as above on first sight seems to be defensive approach to dependency management. We decide to every run of npm install download and install latest release of specified major.minor version. In other worlds, we accept bugfixes to the current version but refuse to download new minor or major versions. As long as it accepted on development environment, this approach needs to be reviewed when production deployment is considered. Deploying on production we want to be a 100% sure that we deploy software package which has been already tested. In that solution we cannot assure that. Running npm install on production will probably download new version (bugfix) of some package.

How to deal with it?There are at least a few possible solutions:

  • use specific version (for example “forever-monitor”: “1.2.3”) in package.json
    • pros
      • every time npm install is executed the same version is downloaded and installed
      • every version change needs editing package.json file
    • cons
      • it is still possible that package author republishes an existing version of the package and as a result we get a different code on production
      • on development environment it is accepted that new patches are installed since the software is instantly tested
  • npm install dependencies on development environment and commit the results into version controlsystem
    • pros
      • we deploy the exact bits which were checked in into version control
    • cons
      • in specific cases npm install not only downloads packages but also builds system-dependent binaries so it is obvious that packages installed on Microsoft Windows system will be suitable for Linux operating system
      • it is error prone since someone can check in a source change but not regenerate the binaries
      • it is redundant since binaries can always be built from sources; as we do not commit built artifacts in Java ecosystem, we should not do that in Node.js environment too
  • npm shrinkwrap – the command locks down the versions of package’s dependencies; produces npm-shrinkwrap.json containg specific version at the current time which is used instead of package.json in subsequent npm install commands
    • pros
      • all dependencies are locked down so every npm install will download the same version
    • cons
      • there is still possibility that package author will republish code while not changing version number
  • npm shrinkwrap on development environment and tar up dependencies on test environment before distributing them to production; test development is the same as production as far as platform is considered
    • pros
      • changes to packages versions are acceptable on development environment (update shrinkwrapped package as described in manual)
      • version deployed on production is always the same version which was tested
    • cons
      • patches are not automatically downloaded every npm install on development environment and require some manual actions, but not in package.json file
This was a quick review of a few available package management methods. Of course there are more possibilities such as maintaining a mirror of the part of npm registry but this a subject for another post. 
Do not expect to find one, always suitable solution. If it is fine by you to specify exact package version you can carry on with it. Personally I prefer the last route (npm shrinkwrap and tar up dependencies).