10 Node.js Best Practices: Enlightenment from the Node Gurus

Azat Mardan
Share

A Street Fighter-like character leaping into the air, gathering energy between his hands - level up your Node skills

10 Node.js Best Practices: Enlightenment from the Node Gurus is by guest author Azat Mardan. SitePoint guest posts aim to bring you engaging content from prominent writers and speakers of the Web community.

In my previous article 10 Tips to Become a Better Node Developer in 2017, I introduced 10 Node.js tips, tricks and techniques you could apply to your code today. This post continues in that vein with a further 10 best practices to help you take your Node skills to the next level. This is what we’re going to cover:

  1. Use npm scripts — Stop writing bash scripts when you can organize them better with npm scripts and Node. E.g., npm run build, start and test. npm scripts are like the single source of truth when Node developers look at a new project.
  2. Use env vars — Utilize process.env.NODE_ENV by setting it to development, or production. Some frameworks will use this variable too, so play by the convention.
  3. Understand the event loopsetImmediate() is not immediate while nextTick() is not next. Use setImmediate() or setTimeout() to offload CPU-intensive tasks to the next event loop cycle.
  4. Use functional inheritance — Avoid getting into mindless debates and a brain-draining trap of debugging and understanding prototypal inheritance or classes by just using functional inheritance like some of the most prolific Node contributors do.
  5. Name things appropriately — Give meaningful names which will serve as a documentation. Also, please no uppercase filenames, use a dash if needed. Uppercase in filenames not just look strange but can cause cross-platform issues.
  6. Consider NOT Using JavaScript — ES6/7 is pathetic addition which was born out of 6 years of meetings when we already had a better JavaScript called CoffeeScript. Use it if you want ship code faster and stop wasting time debating var/const/let, semi-colons, class and other arguments.
  7. Provide native code — When using transpilers, commit native JS code (result of the builds) so your projects can run without the builds
  8. Use gzip — Duh! npm i compression -S and sane logging — not too much not to little depending on the environment. npm i morgan -S
  9. Scale up — Start thinking about clustering and having stateless services from day one of your Node development. Use pm2 or strongloop’s cluster control
  10. Cache requests — Get maximum juice out of your Node servers by hiding them behind a static file server such as nginx and/or request level cache like Varnish Cache and CDN caching.

So let’s bisect and take a look at each one of them individually. Shall we?

Use npm Scripts

It’s almost a standard now to create npm scripts for builds, tests, and most importantly to start the app. This is the first place Node developers look at when they encounter a new Node project. Some people (1, 2, 3, 4) have even ditched Grunt, Gulp and the likes for the more low-level but more dependable npm script. I can totally understand their argument. Considering that npm scripts have pre and post hooks, you can get to a very sophisticated level of automation:

"scripts": {
  "preinstall": "node prepare.js",
  "postintall": "node clean.js",
  "build": "webpack",
  "postbuild": "node index.js",
  "postversion": "npm publish"
}

Often times when developing for the front-end, you want to run two or more watch processes to re-build your code. For example, one for webpack and another for nodemon. You can do this with && since the first command won’t release the prompt. However, there’s a handy module called concurrently which can spawn multiple processes and run them at the same time.

Also, install dev command line tools such as webpack, nodemon, gulp, Mocha, etc. locally to avoid conflicts. You can point to ./node_modules/.bin/mocha for example or add this line to your bash/zsh profile (PATH!):

export PATH="./node_modules/.bin:$PATH"

Use Env Vars

Utilize environment variables even for the early stages of a project to ensure there’s no leakage of sensitive info, and just to build the code properly from the beginning. Moreover, some libraries and frameworks (I know Express does it for sure) will pull in info like NODE_ENV to modify their behavior. Set it to production. Set your MONGO_URI and API_KEY values as well. You can create a shell file (e.g. start.sh) and add it to .gitignore:

NODE_ENV=production MONGO_URL=mongo://localhost:27017/accounts API_KEY=lolz nodemon index.js

Nodemon also has a config file where you can put your env vars (example):

{
  "env": {
    "NODE_ENV": "production",
    "MONGO_URL": "mongo://localhost:27017/accounts"
  }
}

Understand the Event Loop

The mighty and clever event loop is what makes Node so fast and brilliant by utilizing all the time which would have been wasted waiting for input and output tasks to complete. Thus, Node is great at optimizing I/O-bound systems.

If you need to perform something CPU-intensive (e.g., computation, hashing of passwords, or compressing), then in addition to spawning new processes for those CPU-tasks, you might want to explore the deferring of the task with setImmediate() or setTimeout() — the code in their callbacks will continue on the next event loop cycle. nextTick() works on the same cycle contrary to the name. Argh!

Here’s a diagram from Bert Belder who worked on the event loop. He clearly knows how the event loop works!

The event loop

Use Functional Inheritance

JavaScript support prototypal inheritance which is when objects inherit from other objects. The class operator was also added to the language with ES6. However, it’s overtly complex compared to functional inheritance. Most Node gurus prefer the simplicity of the latter. It’s implemented by a simple function factory pattern, and does NOT require the use of prototype, new or this. There are no implicit effects when you update the prototype (causing all the instances to change as well) since in functional inheritance each object uses its own copy of methods.

Consider code from TJ Holowaychuk, the prolific genius behind Express, Mocha, Connect, Superagent and dozens of other Node modules. Express uses functional inheritance (full source code):

exports = module.exports = createApplication;
// ...
function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  app.request = { __proto__: req, app: app };
  app.response = { __proto__: res, app: app };
  app.init();
  return app;
}

To be objective, core Node modules use prototypal inheritance a lot. If you follow that pattern, make sure you know how it works. You can read more about JavaScript inheritance patterns here.

Name Things Appropriately

This one is obvious. Good names serve as a documentation. Which one would you prefer?

const dexter = require('morgan')
// ...
app.use(dexter('dev')) // When is the next season?

I have no idea what dexter is doing when I only look at app.use(). How about a different more meaningfulname:

const logger = require('morgan')
// ...
app.use(logger('dev')) // Aha!

In the same fashion, file names must correctly reflect what is the purpose of the code inside. If you take a look at the lib folder of Node (GitHub link) which has all the core modules bundled with the platform, then you will see clear naming of the files/modules (even if you are not very familiar with all the core modules):

events.js
fs.js
http.js
https.js
module.js
net.js
os.js
path.js
process.js
punycode.js
querystring.js

The internal modules are marked with an underscore (_debugger.js, _http_agent.js, _http_client.js) just like methods and variable in the code. This helps to warn developers that this is an internal interface and if you are using it, you are on your own — don’t complain if it gets refactored or even removed.

Consider NOT Using JavaScript

Huh? Did you just read it correctly? But what the heck? Yes. That’s correct. Even with ES6 and the two features added by ES2016/ES7, JavaScript still has its quirks. There are other options besides JavaScript which you or your team can benefit from with very little setup. Depending on the expertise level and the nature of the app, you might be better off with TypeScript or Flow which provide strong typing. On the other end of the spectrum, there’s Elm or ClojureScript which are purely functional. CoffeeScript is another great and battle-tested option. You might take a look at Dart 2.0 as well.

When all you need is just a few macros (macros allow you to build exactly the language you want), not an entire new language, then consider Sweet.js which will do exactly that — allow you to write code which generates code.

If you go the non-JavaScript route, please still include your compiled code because some developers might not understand your language well enough to build it properly. For example, VS Code is one of the largest TypeScript projects, maybe after Angular 2, and Code uses TypeScript to patch Node’s core module with types. In the vscode/src/vs/base/node/ of VS Code repo (link), you can see familiar module names like crypto, process, etc. but with the ts extension. There are other ts files in the repo. However, they also included vscode/build with native JavaScript code.

Know Express Middleware

Express is a great and very mature framework. It’s brilliance comes from allowing myriads of other modules to configure its behavior. Thus, you need to know the most used middleware and you need to know how to use it. So why not grab my Express cheat sheet. I have the main middleware modules listed there. For example, npm i compression -S will give reduce the download speed by deflating the responses. logger('tiny') or logger('common') will provide less (dev) or more (prod) logs respectively.

Scale up

Node is great at async due to its non-blocking I/O and it keeps this async way of coding simple because there’s just one thread. This is an opportunity to start scaling early on, maybe even with the first lines of code. There’s the core cluster module which will allow you to scale vertically without too many problems. However, an even better way would be is to use a tool such as pm2 or StrongLoop’s cluster control.

For example, this is how you can get started with pm2:

npm i -g pm2

Then you can start four instances of the same server:

pm2 start server.js -i 4

For Docker, pm2 version 2+ has pm2-docker. So your Dockerfile can look like this:

# ...

RUN npm install pm2 -g

CMD ["pm2-docker", "app.js"]

The official Alpine Linux pm2 image is in the Docker Hub.

Cache Requests

This is a DevOps best practice which will allow you to get more juice out of your Node instances (you get more than one with pm2 or the like, see above). The way to go is to let Node servers do app stuff like making requests, processing data and executing business logic and offload the traffic to static files to another web server such as Apache httpd or Nginx. Again, you probably should use Docker for the set up:

FROM nginx

COPY nginx.conf /etc/nginx/nginx.conf

I like to use Docker compose to make multiple containers (nginx, Node, Redis, MongoDB) work with each other. For example:

web:
  build: ./app
  volumes:
    - "./app:/src/app"
  ports:
    - "3030:3000"
  links:
    - "db:redis"
  command: pm2-docker app/server.js

nginx:
  restart: always
  build: ./nginx/
  ports:
    - "80:80"
  volumes:
    - /www/public
  volumes_from:
    - web
  links:
    - web:web

db:
  image: redis

Summary

In this day and age of open-source software, there are no excuses not to learn from the trusted and tested code which is out in the open. You don’t need to be in the inner circle to get in. Learning never stops and I’m sure soon we will have different best practices based on the failures and successes which we will experience. They are guaranteed.

Finally, I wanted to write about how software is eating the world and how JavaScript is eating the software… there are great things like yearly standard releases, lots and lots of npm modules, tools and conferences… but instead I’ll finish with a word of caution.

I see how more and more people chase the next new framework or language. It’s the shiny object syndrome. They learn a new library every week and a new framework every month. They compulsively check Twitter, Reddit, Hacker News and JS Weekly. They use the overwhelming level of activity in the JavaScript world to procrastinate. They have empty public GitHub histories.

Learning new things is good but don’t confuse it for actually building stuff. What matters and what pays your salary is actually building things. Stop over engineering. You’re not building the next Facebook. Promises vs. generators vs. async await is a moot for me, because by the time someone replied to a thread in a discussion, I already wrote my callback (and used CoffeeScript to do it 2x faster than in plain ES5/6/7!).

The final best practice is to use best practices and the best of the best is to master fundamentals. Read source code, try new things in code and most importantly write tons of code yourself. Now, at this point, stop reading and go ship code that matters!

And just in case this post is not enough here is some more reading on best Node practices: