NodeJS : Best Practices for Production
This is an attempt to enlist the most important practices to be followed while developing and deploying on NodeJs.
I have been working on this technology for a while myself and have realised it’s huge potential and place in the development process. With tough competition with languages like Python and Golang, NodeJS has proven it’s utility in appropriate use cases.
Before I delve into the best practices 😬, I would like to introduce in brief what microservice pattern is and take the conversation further from there on.
So, what are microservices?
Microservices - also known as the microservice architecture - is an architectural style that structures an application as a collection of services that are
- Highly maintainable and testable
- Loosely coupled
- Independently deployable
- Organized around business capabilities.
The microservice architecture enables the continuous delivery/deployment of large, complex applications. It also enables an organization to evolve its technology stack.
How to decide if you need microservices?
Initially, when you are just starting out to work on your MVP , you might not need to use microservices. The Y-axis scaling might not be your agenda right now. But when the product starts to mature and sometimes too early where you have to deal with scaling, the decomposition into functional modules makes higher sense as the business itself is decomposing. This will be the right point to start looking into microservices architecture pattern.
A book that I highly recommend is by Chris Richardson himself , http://bit.ly/2EmJDYt
Microservices pattern is most commonly considered while replacing a monolithic application that used to be pretty common until recently when containerization solutions like docker is ruling the DevOps world, but more on that later.
It would be unfair if I continue without mentioning about Domain Driven Design (DDD) which is very popular strategy for decomposing your product into functional modules and hence very useful to create microservices.
So, what is a domain as per DDD?
Each problem that you are trying to solve is a domain.
Each domain is subdivided into mutually exclusive bounded contexts which are nothing but separate areas of that particular problem.
In microservice pattern, each bounded context correlates to a microservice. DDD patterns help you understand the complexity in the domain. For the domain model for each Bounded Context, you identify and define the
entities, value objects, and aggregates that model your domain.
Depending on the complexity of your software you can choose the DDD principles or perform a simpler approach.
The goal is to achieve highly cohesive and loosely coupled domain model and for that follow this approach :
This was brief intro on the DDD. To learn more about it , it’s highly recommended to read Eric Evans’s excellent book http://bit.ly/2Eoy17l
I hope you are holding on with me. 😜
So from here on , I will talk more about practices specific to NodeJS. And what I mean is that, microservices and DDD help you benchmark the true potential of NodeJS nevertheless, it’s complete in itself. How? We will see..
Which Design Pattern to use while using NodeJs?
Design Pattern – is about designing a software using certain standards that are known to several developers.
There are various design patterns we can use. I would like to introduce and/or recap for developers who already know about a pattern called Repository Pattern.
This pattern makes it easier to separate the MVC logic while also making it easier to handle model definition and model interaction with rest of the logic.
It consists of :
1. Controller : It only and only handles the request and response and associated attributes. It will not have any business logic or model definition or model associations too. (folder name : controllers)
2. Service: It contains business logic for your microservice. The control passes from controller to a service. It’s a 1:1 relationship between a controller and its service and a 1: many relationship between a service and repositories. (folder name : services)
3. Repository: It interacts with the models that is part of the model folder. Any query to the database through the model layer will be formed here. It will not have any business logic. (folder name : repositories)
4. Model: It contains model definition, associations, virtual functions(eg. in mongoose)
5. Utilities: This will contain helper classes/functions that can be used services. Eg: a redis utility that has all the functions required to interact with redis . (folder name : utilities)
6. Test case: This will include unit test cases against controller methods to ensure maximum code coverage. (folder name : spec)
To read more on this ,you can refer http://bit.ly/2TrSyRS
Ok, Tell me about cluster module?
A single instance of Node.js runs in a single thread. To take advantage of multi-core systems, the user will sometimes want to launch a cluster of Node.js processes to handle the load.
The cluster module allows easy creation of child processes that all share server ports.
Please note that it’s ideal to use one process per container while using docker containerization for deployment through microservices. Hence, cluster module isn’t useful when using docker-ization.
How to handle control flow in NodeJS?
While using callbacks or promises following
libraries could be useful
1. Async (https://www.npmjs.com/package/async)
2. Vasync( with better tracking of operation) https://www.npmjs.com/package/vasync
3. Bluebird ( handle promises eg. Promise.all etc.) https://www.npmjs.com/package/bluebird
- Series loop : executing each step one by one in order
- Delayed loop : loop with a timeout
- Parallel loop : collecting all promises in a loop and execute in parallel
And what are some useful linting tools?
Lint tools analyze your code statically (without running it) to identify potential bugs or dangerous patterns, like the use of undeclared variables or "case" statements inside a switch without a "break" statement.
Ok, how to handle Logging ?
Commonly used npm packages -
• Possible logging format –
For distributed systems like microservices, you would like to explore distributed tracing using ZipKin etc.
A note on NPM packages : You should use a package only if it solves a problem for you that you can’t solve yourself. Regularly perform npm audit to find critical issues with your npm dependencies.
Handling uncaught exception
By default, Node.js handles such exceptions by printing the stack trace
to stderr and exiting with code 1, overriding any previously set process.exitCode
• Adding a handler for the 'uncaughtException' event overrides this default behavior.
Alternatively, change the process.exitCode in the 'uncaughtException' handler
which will result in the process exiting with the provided exit code. Otherwise, in the presence of such handler the process will exit with 0.
process.exit(0) – successful termination
process.exit(1) – unsuccessful termination
Handling unhandled rejection
Promises are ubiquitous in Node.js code and sometimes chained to a very long list of functions that return promises and so on.
Not using a proper .catch(…) rejection handler will cause an unhandledRejection event to be emitted, and if not properly caught and inspected, you may rob yourself of your only chance to detect and possibly fix the problem.
console.time() and console.timeEnd()
The console object has time() and timeEnd() methods that help with analyzing
performance of pieces of your code.
This is not a solution for production but it can be used when you don’t have better tools.
Thank you for your time. See you soon!