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

Share this article

A Street Fighter-like character leaping into the air, gathering energy between his hands

Key Takeaways

  • Utilize npm scripts for organizing tasks such as builds, tests, and starting the app. This provides a single source of truth when developers look at a new project.
  • Employ environment variables, such as process.env.NODE_ENV, from the early stages of a project. This ensures no leakage of sensitive info and builds the code properly from the start.
  • Understand the event loop and how to use setImmediate() or setTimeout() to offload CPU-intensive tasks to the next event loop cycle.
  • Use functional inheritance for simplicity and to avoid the complexities of prototypal inheritance or classes. This is a preferred method among many prolific Node contributors.
  • Consider alternatives to JavaScript, such as TypeScript, Flow, Elm, ClojureScript, or CoffeeScript depending on the expertise level and the nature of the app. This can potentially benefit the team with very little setup.

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:

Frequently Asked Questions (FAQs) on Node.js Best Practices

What are some of the most important best practices for Node.js development?

Node.js development involves several best practices that can significantly enhance the efficiency and scalability of your applications. These include using asynchronous programming, which allows for non-blocking operations and improves performance. It’s also crucial to handle errors properly to prevent application crashes. Other best practices include using a linter to enforce code quality, using environment variables for configuration, and writing small modules to keep your codebase manageable and understandable.

How can I improve the performance of my Node.js application?

There are several ways to improve the performance of your Node.js application. One of the most effective methods is to use the Cluster module, which allows you to create child processes that all share server ports. This can significantly improve the performance of your application by allowing it to handle more requests simultaneously. Additionally, you can use tools like PM2 to manage and monitor your Node.js applications, which can help you identify and address performance issues.

What are some common mistakes to avoid when developing with Node.js?

Some common mistakes to avoid when developing with Node.js include blocking the event loop, not handling errors properly, and not using tools like linters to enforce code quality. Blocking the event loop can lead to performance issues, as it prevents other operations from being executed. Not handling errors properly can lead to application crashes, while not using linters can lead to inconsistent code quality and potential bugs.

How can I ensure that my Node.js application is secure?

Ensuring the security of your Node.js application involves several best practices. These include using HTTPS for secure communication, validating and sanitizing user input to prevent injection attacks, and using security headers to protect against common web vulnerabilities. It’s also important to keep your dependencies up to date, as outdated dependencies can contain known security vulnerabilities.

What are some best practices for testing Node.js applications?

Testing is a crucial part of Node.js development, and there are several best practices to follow. These include writing unit tests to test individual components of your application, integration tests to test how these components interact, and end-to-end tests to test your application as a whole. It’s also important to use a continuous integration (CI) system to automatically run your tests whenever changes are made to your codebase.

How can I manage dependencies in Node.js?

Managing dependencies in Node.js is typically done using npm, the default package manager for Node.js. It’s important to specify exact versions of your dependencies in your package.json file to ensure that your application works as expected. You should also regularly update your dependencies to benefit from bug fixes and security patches.

What are some best practices for error handling in Node.js?

Error handling is a crucial part of Node.js development. Best practices include using try/catch blocks to catch synchronous errors, using error-first callbacks to handle asynchronous errors, and using a centralized error handling mechanism to handle all errors in one place. It’s also important to log errors for debugging purposes and to respond to the client with appropriate error messages.

How can I ensure code quality in Node.js?

Ensuring code quality in Node.js involves several best practices. These include using a linter to enforce code quality, following a consistent coding style, and writing tests to catch bugs early. It’s also important to use version control (like Git) to track changes to your codebase and to perform code reviews to catch potential issues.

How can I scale my Node.js application?

Scaling a Node.js application can be achieved in several ways. One common method is to use the Cluster module to create child processes that share server ports, allowing your application to handle more requests simultaneously. You can also use load balancing to distribute incoming network traffic across multiple servers, and horizontal scaling (adding more machines) or vertical scaling (adding more resources to a single machine) depending on your needs.

What are some best practices for deploying Node.js applications?

Deploying Node.js applications involves several best practices. These include using environment variables for configuration, using a process manager like PM2 to manage your application, and using a continuous integration (CI) system to automatically deploy your application when changes are made to your codebase. It’s also important to monitor your application to identify and address performance issues.

Azat MardanAzat Mardan
View Author

Azat is a Technology Fellow, Manager at Capital One, and a JavaScript/Node.js expert with several online courses on Udemy and Node University, and 12 books published on the topic, including top-sellers React Quickly (Manning, 2016), Full Stack JavaScript (Apress, 2015), Practical Node.js (Apress, 2014) and Pro Express.js (Apress, 2014). In his spare time, Azat writes about tech on Webapplog.com, speaks at conferences and contributes to open-source. Before becoming an expert in Node.js, Azat finished his Master’s in Information Systems Technology; and worked at U.S. federal government agencies, small startups and big corporations with various technologies such as Java, SQL, PHP, Ruby, etc. Azat is passionate about technology and finance, as well as new disruptive ways of educating and empowering people. Website: http://azat.co LinkedIn: http://www.linkedin.com/in/azatm Professional Blog: http://webapplog.com/ Amazon Profile: https://www.amazon.com/Azat-Mardan/e/B00GM6QNI4/

event loopinheritancejameshLearn-Node-JSscaling
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week