Node.js Best Practices
The Node.js best practices list (November 2022)
README
Node.js Best Practices
Read in a different language: [CN](./README.chinese.md), [FR](./README.french.md), [BR](./README.brazilian-portuguese.md), [RU](./README.russian.md), [PL](./README.polish.md), [JA](./README.japanese.md), [EU](./README.basque.md) [(ES, HE, KR and TR in progress! )](#translations)
🚀 We have an official Node.js starter - Practica.js. Use it to generate a new solution skeleton with all the practices baked inside. Or just it to learn by code examples
Latest Best Practices and News
- ✨ 80,000 stars: Blushing, surprised and proud!
- 🔖 New menu and tags: Our menu is collapsible now and includes #tags. New visitors can read #strategic items first. Returning visitors can focus on #new content. Seniors can filter for #advanced items. Courtesy of the one and only Rubek Joshi
- 👨👩👧👦 New family member!: A new repository joins our family - Node.js Integration Tests Best Practices ✨. It includes 40+ best practices for writing awesome and performant Node.js component tests
- French translation!1! : The latest translation that joins our international guide is French. Bienvenue
Welcome! 3 Things You Ought To Know First
1. You are reading dozens of the best Node.js articles - this repository is a summary and curation of the top-ranked content on Node.js best practices, as well as content written here by collaborators
2. It is the largest compilation, and it is growing every week - currently, more than 80 best practices, style guides, and architectural tips are presented. New issues and pull requests are created every day to keep this live book updated. We'd love to see you contributing here, whether that is fixing code mistakes, helping with translations, or suggesting brilliant new ideas. See our writing guidelines here
3. Best practices have additional info - most bullets include a 🔗Read More link that expands on the practice with code examples, quotes from selected blogs, and more information
Table of Contents
1. Project Structure Practices (5)
2. Error Handling Practices (12)
3. Code Style Practices (12)
4. Testing And Overall Quality Practices (13)
5. Going To Production Practices (19)
6. Security Practices (25)
7. Performance Practices (2) (Work In Progress️ ✍️)
8. Docker Practices (15)
1. Project Structure Practices
![✔] 1.1 Structure your solution by components
TL;DR: The worst large applications pitfall is maintaining a huge code base with hundreds of dependencies - such a monolith slows down developers as they try to incorporate new features. Instead, partition your code into components, each gets its folder or a dedicated codebase, and ensure that each unit is kept small and simple. Visit 'Read More' below to see examples of correct project structure
Otherwise: When developers who code new features struggle to realize the impact of their change and fear to break other dependent components - deployments become slower and riskier. It's also considered harder to scale-out when all the business units are not separated
🔗 [Read More: structure by components](./sections/projectstructre/breakintcomponents.md)
![✔] 1.2 Layer your components, keep the web layer within its boundaries
TL;DR: Each component should contain 'layers' - a dedicated object for the web, logic, and data access code. This not only draws a clean separation of concerns but also significantly eases mocking and testing the system. Though this is a very common pattern, API developers tend to mix layers by passing the web layer objects (e.g. Express req, res) to business logic and data layers - this makes your application dependent on and accessible only by specific web frameworks
Otherwise: App that mixes web objects with other layers cannot be accessed by testing code, CRON jobs, triggers from message queues, etc
🔗 [Read More: layer your app](./sections/projectstructre/createlayers.md)
![✔] 1.3 Wrap common utilities as npm packages
TL;DR: In a large app that constitutes a large codebase, cross-cutting-concern utilities like a logger, encryption and alike, should be wrapped by your code and exposed as private npm packages. This allows sharing them among multiple codebases and projects
Otherwise: You'll have to invent your deployment and the dependency wheel
🔗 [Read More: Structure by feature](./sections/projectstructre/wraputilities.md)
![✔] 1.4 Separate Express 'app' and 'server'
TL;DR: Avoid the nasty habit of defining the entire Express app in a single huge file - separate your 'Express' definition to at least two files: the API declaration (app.js) and the networking concerns (WWW). For even better structure, locate your API declaration within components
Otherwise: Your API will be accessible for testing via HTTP calls only (slower and much harder to generate coverage reports). It probably won't be a big pleasure to maintain hundreds of lines of code in a single file
🔗 [Read More: separate Express 'app' and 'server'](./sections/projectstructre/separateexpress.md)
![✔] 1.5 Use environment aware, secure and hierarchical config
TL;DR: A perfect and flawless configuration setup should ensure (a) keys can be read from file AND from environment variable (b) secrets are kept outside committed code (c) config is hierarchical for easier findability. There are a few packages that can help tick most of those boxes like rc, nconf, config, and convict.
Otherwise: Failing to satisfy any of the config requirements will simply bog down the development or DevOps team. Probably both
🔗 [Read More: configuration best practices](./sections/projectstructre/configguide.md)
2. Error Handling Practices
![✔] 2.1 Use Async-Await or promises for async error handling
TL;DR: Handling async errors in callback style is probably the fastest way to hell (a.k.a the pyramid of doom). The best gift you can give to your code is using a reputable promise library or async-await instead which enables a much more compact and familiar code syntax like try-catch
Otherwise: Node.js callback style, function(err, response), is a promising way to un-maintainable code due to the mix of error handling with casual code, excessive nesting, and awkward coding patterns
🔗 [Read More: avoiding callbacks](./sections/errorhandling/asyncerrorhandling.md)
![✔] 2.2 Use only the built-in Error object
TL;DR: Many throw errors as a string or as some custom type – this complicates the error handling logic and the interoperability between modules. Whether you reject a promise, throw an exception or emit an error – using only the built-in Error object (or an object that extends the built-in Error object) will increase uniformity and prevent loss of information. There is no-throw-literal ESLint rule that strictly checks that (although it have some limitations which can be solved when using TypeScript and setting the@typescript-eslint/no-throw-literal rule)
Otherwise: When invoking some component, being uncertain which type of errors come in return – it makes proper error handling much harder. Even worse, using custom types to describe errors might lead to loss of critical error information like the stack trace!
🔗 [Read More: using the built-in error object](./sections/errorhandling/useonlythebuiltinerror.md)
![✔] 2.3 Distinguish operational vs programmer errors
TL;DR: Operational errors (e.g. API received an invalid input) refer to known cases where the error impact is fully understood and can be handled thoughtfully. On the other hand, programmer error (e.g. trying to read an undefined variable) refers to unknown code failures that dictate to gracefully restart the application
Otherwise: You may always restart the application when an error appears, but why let ~5000 online users down because of a minor, predicted, operational error? The opposite is also not ideal – keeping the application up when an unknown issue (programmer error) occurred might lead to an unpredicted behavior. Differentiating the two allows acting tactfully and applying a balanced approach based on the given context
🔗 [Read More: operational vs programmer error](./sections/errorhandling/operationalvsprogrammererror.md)
![✔] 2.4 Handle errors centrally, not within a middleware
TL;DR: Error handling logic such as mail to admin and logging should be encapsulated in a dedicated and centralized object that all endpoints (e.g. Express middleware, cron jobs, unit-testing) call when an error comes in
Otherwise: Not handling errors within a single place will lead to code duplication and probably to improperly handled errors
🔗 [Read More: handling errors in a centralized place](./sections/errorhandling/centralizedhandling.md)
![✔] 2.5 Document API errors using Swagger or GraphQL
TL;DR: Let your API callers know which errors might come in return so they can handle these thoughtfully without crashing. For RESTful APIs, this is usually done with documentation frameworks like Swagger. If you're using GraphQL, you can utilize your schema and comments as well.
Otherwise: An API client might decide to crash and restart only because it received back an error it couldn’t understand. Note: the caller of your API might be you (very typical in a microservice environment)
🔗 [Read More: documenting API errors in Swagger or GraphQL](./sections/errorhandling/documentingusingswagger.md)
![✔] 2.6 Exit the process gracefully when a stranger comes to town
TL;DR: When an unknown error occurs (a developer error, see best practice 2.3) - there is uncertainty about the application healthiness. Common practice suggests restarting the process carefully using a process management tool like Forever or PM2
Otherwise: When an unfamiliar exception occurs, some object might be in a faulty state (e.g. an event emitter which is used globally and not firing events anymore due to some internal failure) and all future requests might fail or behave crazily
🔗 [Read More: shutting the process](./sections/errorhandling/shuttingtheprocess.md)
![✔] 2.7 Use a mature logger to increase error visibility
TL;DR: A set of mature logging tools like Pino or Log4js, will speed-up error discovery and understanding. So forget about console.log
Otherwise: Skimming through console.logs or manually through messy text file without querying tools or a decent log viewer might keep you busy at work until late
🔗 [Read More: using a mature logger](./sections/errorhandling/usematurelogger.md)
![✔] 2.8 Test error flows using your favorite test framework
TL;DR: Whether professional automated QA or plain manual developer testing – Ensure that your code not only satisfies positive scenarios but also handles and returns the right errors. Testing frameworks like Mocha & Chai can handle this easily (see code examples within the "Gist popup")
Otherwise: Without testing, whether automatically or manually, you can’t rely on your code to return the right errors. Without meaningful errors – there’s no error handling
🔗 [Read More: testing error flows](./sections/errorhandling/testingerrorflows.md)
![✔] 2.9 Discover errors and downtime using APM products
TL;DR: Monitoring and performance products (a.k.a APM) proactively gauge your codebase or API so they can automagically highlight errors, crashes, and slow parts that you were missing
Otherwise: You might spend great effort on measuring API performance and downtimes, probably you’ll never be aware which are your slowest code parts under real-world scenario and how these affect the UX
🔗 [Read More: using APM products](./sections/errorhandling/apmproducts.md)
![✔] 2.10 Catch unhandled promise rejections
TL;DR: Any exception thrown within a promise will get swallowed and discarded unless a developer didn’t forget to explicitly handle it. Even if your code is subscribed to process.uncaughtException! Overcome this by registering to the event process.unhandledRejection
Otherwise: Your errors will get swallowed and leave no trace. Nothing to worry about
🔗 [Read More: catching unhandled promise rejection](./sections/errorhandling/catchunhandledpromiserejection.md)
![✔] 2.11 Fail fast, validate arguments using a dedicated library
TL;DR: Assert API input to avoid nasty bugs that are much harder to track later. The validation code is usually tedious unless you are using a very cool helper library like ajv and Joi
Otherwise: Consider this – your function expects a numeric argument “Discount” which the caller forgets to pass, later on, your code checks if Discount!=0 (amount of allowed discount is greater than zero), then it will allow the user to enjoy a discount. OMG, what a nasty bug. Can you see it?
🔗 [Read More: failing fast](./sections/errorhandling/failfast.md)
![✔] 2.12 Always await promises before returning to avoid a partial stacktrace
TL;DR: Always do return await when returning a promise to benefit full error stacktrace. If a
function returns a promise, that function must be declared as async function and explicitly
await the promise before returning it
Otherwise: The function that returns a promise without awaiting won't appear in the stacktrace.
Such missing frames would probably complicate the understanding of the flow that leads to the error,
especially if the cause of the abnormal behavior is inside of the missing function
🔗 [Read More: returning promises](./sections/errorhandling/returningpromises.md)
3. Code Style Practices
![✔] 3.1 Use ESLint
TL;DR: ESLint is the de-facto standard for checking possible code errors and fixing code style, not only to identify nitty-gritty spacing issues but also to detect serious code anti-patterns like developers throwing errors without classification. Though ESLint can automatically fix code styles, other tools like prettier and beautify are more powerful in formatting the fix and work in conjunction with ESLint
Otherwise: Developers will focus on tedious spacing and line-width concerns and time might be wasted overthinking the project's code style
🔗 [Read More: Using ESLint and Prettier](./sections/codestylepractices/eslint_prettier.md)
![✔] 3.2 Node.js specific plugins
TL;DR: On top of ESLint standard rules that cover vanilla JavaScript, add Node.js specific plugins like eslint-plugin-node, eslint-plugin-mocha and eslint-plugin-node-security
Otherwise: Many faulty Node.js code patterns might escape under the radar. For example, developers might require(variableAsPath) files with a variable given as a path which allows attackers to execute any JS script. Node.js linters can detect such patterns and complain early
![✔] 3.3 Start a Codeblock's Curly Braces on the Same Line
TL;DR: The opening curly braces of a code block should be on the same line as the opening statement
Code Example
- ``` js
- // Do
- function someFunction() {
- // code block
- }
- // Avoid
- function someFunction()
- {
- // code block
- }
- ```
Otherwise: Deferring from this best practice might lead to unexpected results, as seen in the StackOverflow thread below:
🔗 [Read more: "Why do results vary based on curly brace placement?" (StackOverflow)](https://stackoverflow.com/questions/3641519/why-does-a-results-vary-based-on-curly-brace-placement)
![✔] 3.4 Separate your statements properly
No matter if you use semicolons or not to separate your statements, knowing the common pitfalls of improper linebreaks or automatic semicolon insertion, will help you to eliminate regular syntax errors.
TL;DR: Use ESLint to gain awareness about separation concerns. Prettier or Standardjs can automatically resolve these issues.
Otherwise: As seen in the previous section, JavaScript's interpreter automatically adds a semicolon at the end of a statement if there isn't one, or considers a statement as not ended where it should, which might lead to some undesired results. You can use assignments and avoid using immediately invoked function expressions to prevent most of the unexpected errors.
Code example
- ``` js
- // Do
- function doThing() {
- // ...
- }
- doThing()
- // Do
- const items = [1, 2, 3]
- items.forEach(console.log)
- // Avoid — throws exception
- const m = new Map()
- const a = [1,2,3]
- [...m.values()].forEach(console.log)
- > [...m.values()].forEach(console.log)
- > ^^^
- > SyntaxError: Unexpected token ...
- // Avoid — throws exception
- const count = 2 // it tries to run 2(), but 2 is not a function
- (function doSomething() {
- // do something amazing
- }())
- // put a semicolon before the immediate invoked function, after the const definition, save the return value of the anonymous function to a variable or avoid IIFEs altogether
- ```
🔗 [Read more: "Semi ESLint rule"](https://eslint.org/docs/rules/semi)
🔗 [Read more: "No unexpected multiline ESLint rule"](https://eslint.org/docs/rules/no-unexpected-multiline)
![✔] 3.5 Name your functions
TL;DR: Name all functions, including closures and callbacks. Avoid anonymous functions. This is especially useful when profiling a node app. Naming all functions will allow you to easily understand what you're looking at when checking a memory snapshot
Otherwise: Debugging production issues using a core dump (memory snapshot) might become challenging as you notice significant memory consumption from anonymous functions
![✔] 3.6 Use naming conventions for variables, constants, functions and classes
TL;DR: Use _lowerCamelCase_ when naming constants, variables and functions, _UpperCamelCase_ (capital first letter as well) when naming classes and _UPPER_SNAKE_CASE_ when naming global or static variables. This will help you to easily distinguish between plain variables, functions, classes that require instantiation and variables declared at global module scope. Use descriptive names, but try to keep them short
Otherwise: JavaScript is the only language in the world that allows invoking a constructor ("Class") directly without instantiating it first. Consequently, Classes and function-constructors are differentiated by starting with UpperCamelCase
3.6 Code Example
- ``` js
- // for global variables names we use the const/let keyword and UPPER_SNAKE_CASE
- let MUTABLE_GLOBAL = "mutable value"
- const GLOBAL_CONSTANT = "immutable value";
- const CONFIG = {
- key: "value",
- };
- // examples of UPPER_SNAKE_CASE convention in nodejs/javascript ecosystem
- // in javascript Math.PI module
- const PI = 3.141592653589793;
- // https://github.com/nodejs/node/blob/b9f36062d7b5c5039498e98d2f2c180dca2a7065/lib/internal/http2/core.js#L303
- // in nodejs http2 module
- const HTTP_STATUS_OK = 200;
- const HTTP_STATUS_CREATED = 201;
- // for class name we use UpperCamelCase
- class SomeClassExample {
- // for static class properties we use UPPER_SNAKE_CASE
- static STATIC_PROPERTY = "value";
- }
- // for functions names we use lowerCamelCase
- function doSomething() {
- // for scoped variable names we use the const/let keyword and lowerCamelCase
- const someConstExample = "immutable value";
- let someMutableExample = "mutable value";
- }
- ```
![✔] 3.7 Prefer const over let. Ditch the var
TL;DR: Using const means that once a variable is assigned, it cannot be reassigned. Preferring const will help you to not be tempted to use the same variable for different uses, and make your code clearer. If a variable needs to be reassigned, in a for loop, for example, use let to declare it. Another important aspect of let is that a variable declared using it is only available in the block scope in which it was defined. var is function scoped, not block-scoped, and shouldn't be used in ES6 now that you haveconst and let at your disposal
Otherwise: Debugging becomes way more cumbersome when following a variable that frequently changes
🔗 [Read more: JavaScript ES6+: var, let, or const? ](https://medium.com/javascript-scene/javascript-es6-var-let-or-const-ba58b8dcde75)
![✔] 3.8 Require modules first, not inside functions
TL;DR: Require modules at the beginning of each file, before and outside of any functions. This simple best practice will not only help you easily and quickly tell the dependencies of a file right at the top but also avoids a couple of potential problems
Otherwise: Requires are run synchronously by Node.js. If they are called from within a function, it may block other requests from being handled at a more critical time. Also, if a required module or any of its dependencies throw an error and crash the server, it is best to find out about it as soon as possible, which might not be the case if that module is required from within a function
![✔] 3.9 Require modules by folders, as opposed to the files directly
TL;DR: When developing a module/library in a folder, place an index.js file that exposes the module's internals so every consumer will pass through it. This serves as an 'interface' to your module and eases future changes without breaking the contract
Otherwise: Changing the internal structure of files or the signature may break the interface with clients
3.9 Code example
- ``` js
- // Do
- module.exports.SMSProvider = require("./SMSProvider");
- module.exports.SMSNumberResolver = require("./SMSNumberResolver");
- // Avoid
- module.exports.SMSProvider = require("./SMSProvider/SMSProvider.js");
- module.exports.SMSNumberResolver = require("./SMSNumberResolver/SMSNumberResolver.js");
- ```
![✔] 3.10 Use the === operator
TL;DR: Prefer the strict equality operator === over the weaker abstract equality operator ==. == will compare two variables after converting them to a common type. There is no type conversion in ===, and both variables must be of the same type to be equal
Otherwise: Unequal variables might return true when compared with the == operator
3.10 Code example
- ``` js
- "" == "0"; // false
- 0 == ""; // true
- 0 == "0"; // true
- false == "false"; // false
- false == "0"; // true
- false == undefined; // false
- false == null; // false
- null == undefined; // true
- " \t\r\n " == 0; // true
- ```
All statements above will return false if used with ===
![✔] 3.11 Use Async Await, avoid callbacks
TL;DR: Node 8 LTS now has full support for Async-await. This is a new way of dealing with asynchronous code which supersedes callbacks and promises. Async-await is non-blocking, and it makes asynchronous code look synchronous. The best gift you can give to your code is using async-await which provides a much more compact and familiar code syntax like try-catch
Otherwise: Handling async errors in callback style are probably the fastest way to hell - this style forces to check errors all over, deal with awkward code nesting, and makes it difficult to reason about the code flow
🔗[Read more: Guide to async-await 1.0](https://github.com/yortus/asyncawait)
![✔] 3.12 Use arrow function expressions (=>)
TL;DR: Though it's recommended to use async-await and avoid function parameters when dealing with older APIs that accept promises or callbacks - arrow functions make the code structure more compact and keep the lexical context of the root function (i.e. this)
Otherwise: Longer code (in ES5 functions) is more prone to bugs and cumbersome to read
🔗 [Read more: It’s Time to Embrace Arrow Functions](https://medium.com/javascript-scene/familiarity-bias-is-holding-you-back-its-time-to-embrace-arrow-functions-3d37e1a9bb75)
4. Testing And Overall Quality Practices
![✔] 4.1 At the very least, write API (component) testing
TL;DR: Most projects just don't have any automated testing due to short timetables or often the 'testing project' ran out of control and was abandoned. For that reason, prioritize and start with API testing which is the easiest way to write and provides more coverage than unit testing (you may even craft API tests without code using tools like Postman). Afterwards, should you have more resources and time, continue with advanced test types like unit testing, DB testing, performance testing, etc
Otherwise: You may spend long days on writing unit tests to find out that you got only 20% system coverage
![✔] 4.2 Include 3 parts in each test name
TL;DR: Make the test speak at the requirements level so it's self-explanatory also to QA engineers and developers who are not familiar with the code internals. State in the test name what is being tested (unit under test), under what circumstances, and what is the expected result
Otherwise: A deployment just failed, a test named “Add product” failed. Does this tell you what exactly is malfunctioning?
🔗 [Read More: Include 3 parts in each test name](./sections/testingandquality/3-parts-in-name.md)
![✔] 4.3 Structure tests by the AAA pattern
TL;DR: Structure your tests with 3 well-separated sections: Arrange, Act & Assert (AAA). The first part includes the test setup, then the execution of the unit under test, and finally the assertion phase. Following this structure guarantees that the reader spends no brain CPU on understanding the test plan
Otherwise: Not only you spend long daily hours on understanding the main code, but now also what should have been the simple part of the day (testing) stretches your brain
🔗 [Read More: Structure tests by the AAA pattern](./sections/testingandquality/aaa.md)
![✔] 4.4 Detect code issues with a linter
TL;DR: Use a code linter to check the basic quality and detect anti-patterns early. Run it before any test and add it as a pre-commit git-hook to minimize the time needed to review and correct any issue. Also check Section 3 on Code Style Practices
Otherwise: You may let pass some anti-pattern and possible vulnerable code to your production environment.
![✔] 4.5 Avoid global test fixtures and seeds, add data per-test
TL;DR: To prevent test coupling and easily reason about the test flow, each test should add and act on its own set of DB rows. Whenever a test needs to pull or assume the existence of some DB data - it must explicitly add that data and avoid mutating any other records
Otherwise: Consider a scenario where deployment is aborted due to failing tests, team is now going to spend precious investigation time that ends in a sad conclusion: the system works well, the tests however interfere with each other and break the build
🔗 [Read More: Avoid global test fixtures](./sections/testingandquality/avoid-global-test-fixture.md)
![✔] 4.6 Constantly inspect for vulnerable dependencies
TL;DR: Even the most reputable dependencies such as Express have known vulnerabilities. This can get easily tamed using community and commercial tools such as 🔗 npm audit and 🔗 snyk.io that can be invoked from your CI on every build
Otherwise: Keeping your code clean from vulnerabilities without dedicated tools will require to constantly follow online publications about new threats. Quite tedious
![✔] 4.7 Tag your tests
TL;DR: Different tests must run on different scenarios: quick smoke, IO-less, tests should run when a developer saves or commits a file, full end-to-end tests usually run when a new pull request is submitted, etc. This can be achieved by tagging tests with keywords like #cold #api #sanity so you can grep with your testing harness and invoke the desired subset. For example, this is how you would invoke only the sanity test group with Mocha: mocha --grep 'sanity'
Otherwise: Running all the tests, including tests that perform dozens of DB queries, any time a developer makes a small change can be extremely slow and keeps developers away from running tests
![✔] 4.8 Check your test coverage, it helps to identify wrong test patterns
TL;DR: Code coverage tools like Istanbul/NYC are great for 3 reasons: it comes for free (no effort is required to benefit this reports), it helps to identify a decrease in testing coverage, and last but not least it highlights testing mismatches: by looking at colored code coverage reports you may notice, for example, code areas that are never tested like catch clauses (meaning that tests only invoke the happy paths and not how the app behaves on errors). Set it to fail builds if the coverage falls under a certain threshold
Otherwise: There won't be any automated metric telling you when a large portion of your code is not covered by testing
![✔] 4.9 Inspect for outdated packages
TL;DR: Use your preferred tool (e.g. npm outdated or npm-check-updates) to detect installed outdated packages, inject this check into your CI pipeline and even make a build fail in a severe scenario. For example, a severe scenario might be when an installed package is 5 patch commits behind (e.g. local version is 1.3.1 and repository version is 1.3.8) or it is tagged as deprecated by its author - kill the build and prevent deploying this version
Otherwise: Your production will run packages that have been explicitly tagged by their author as risky
![✔] 4.10 Use production-like environment for e2e testing
TL;DR: End to end (e2e) testing which includes live data used to be the weakest link of the CI process as it depends on multiple heavy services like DB. Use an environment which is as close to your real production environment as possible like a-continue (Missed -continue here, needs content. Judging by the Otherwise clause, this should mention docker-compose)
Otherwise: Without docker-compose, teams must maintain a testing DB for each testing environment including developers' machines, keep all those DBs in sync so test results won't vary across environments
![✔] 4.11 Refactor regularly using static analysis tools
TL;DR: Using static analysis tools helps by giving objective ways to improve code quality and keeps your code maintainable. You can add static analysis tools to your CI build to fail when it finds code smells. Its main selling points over plain linting are the ability to inspect quality in the context of multiple files (e.g. detect duplications), perform advanced analysis (e.g. code complexity), and follow the history and progress of code issues. Two examples of tools you can use are Sonarqube (2,600+ stars) and Code Climate (1,500+ stars).
Otherwise: With poor code quality, bugs and performance will always be an issue that no shiny new library or state of the art features can fix
🔗 [Read More: Refactoring!](./sections/testingandquality/refactoring.md)
![✔] 4.12 Carefully choose your CI platform (Jenkins vs CircleCI vs Travis vs Rest of the world)
TL;DR: Your continuous integration platform (CICD) will host all the quality tools (e.g. test, lint) so it should come with a vibrant ecosystem of plugins. Jenkins used to be the default for many projects as it has the biggest community along with a very powerful platform at the price of a complex setup that demands a steep learning curve. Nowadays, it has become much easier to set up a CI solution using SaaS tools like CircleCI and others. These tools allow crafting a flexible CI pipeline without the burden of managing the whole infrastructure. Eventually, it's a trade-off between robustness and speed - choose your side carefully
Otherwise: Choosing some niche vendor might get you blocked once you need some advanced customization. On the other hand, going with Jenkins might burn precious time on infrastructure setup
🔗 [Read More: Choosing CI platform](./sections/testingandquality/citools.md)
![✔] 4.13 Test your middlewares in isolation
TL;DR: When a middleware holds some immense logic that spans many requests, it is worth testing it in isolation without waking up the entire web framework. This can be easily achieved by stubbing and spying on the {req, res, next} objects
Otherwise: A bug in Express middleware === a bug in all or most requests
🔗 [Read More: Test middlewares in isolation](./sections/testingandquality/test-middlewares.md)
5. Going To Production Practices
![✔] 5.1. Monitoring
TL;DR: Monitoring is a game of finding out issues before customers do – obviously this should be assigned unprecedented importance. The market is overwhelmed with offers thus consider starting with defining the basic metrics you must follow (my suggestions inside), then go over additional fancy features and choose the solution that ticks all boxes. Click ‘The Gist’ below for an overview of the solutions
Otherwise: Failure === disappointed customers. Simple
🔗 [Read More: Monitoring!](./sections/production/monitoring.md)
![✔] 5.2. Increase transparency using smart logging
TL;DR: Logs can be a dumb warehouse of debug statements or the enabler of a beautiful dashboard that tells the story of your app. Plan your logging platform from day 1: how logs are collected, stored and analyzed to ensure that the desired information (e.g. error rate, following an entire transaction through services and servers, etc) can really be extracted
Otherwise: You end up with a black box that is hard to reason about, then you start re-writing all logging statements to add additional information
🔗 [Read More: Increase transparency using smart logging](./sections/production/smartlogging.md)
![✔] 5.3. Delegate anything possible (e.g. gzip, SSL) to a reverse proxy
TL;DR: Node is awfully bad at doing CPU intensive tasks like gzipping, SSL termination, etc. You should use ‘real’ middleware services like nginx, HAproxy or cloud vendor services instead
Otherwise: Your poor single thread will stay busy doing infrastructural tasks instead of dealing with your application core and performance will degrade accordingly
🔗 [Read More: Delegate anything possible (e.g. gzip, SSL) to a reverse proxy](./sections/production/delegatetoproxy.md)
![✔] 5.4. Lock dependencies
TL;DR: Your code must be identical across all environments, but amazingly npm lets dependencies drift across environments by default – when you install packages at various environments it tries to fetch packages’ latest patch version. Overcome this by using npm config files, .npmrc, that tell each environment to save the exact (not the latest) version of each package. Alternatively, for finer grained control use npm shrinkwrap. \*Update: as of NPM5, dependencies are locked by default. The new package manager in town, Yarn, also got us covered by default
Otherwise: QA will thoroughly test the code and approve a version that will behave differently in production. Even worse, different servers in the same production cluster might run different code
🔗 [Read More: Lock dependencies](./sections/production/lockdependencies.md)
![✔] 5.5. Guard process uptime using the right tool
TL;DR: The process must go on and get restarted upon failures. For simple scenarios, process management tools like PM2 might be enough but in today's ‘dockerized’ world, cluster management tools should be considered as well
Otherwise: Running dozens of instances without a clear strategy and too many tools together (cluster management, docker, PM2) might lead to DevOps chaos
🔗 [Read More: Guard process uptime using the right tool](./sections/production/guardprocess.md)
![✔] 5.6. Utilize all CPU cores
TL;DR: At its basic form, a Node app runs on a single CPU core while all others are left idling. It’s your duty to replicate the Node process and utilize all CPUs – For small-medium apps you may use Node Cluster or PM2. For a larger app consider replicating the process using some Docker cluster (e.g. K8S, ECS) or deployment scripts that are based on Linux init system (e.g. systemd)
Otherwise: Your app will likely utilize only 25% of its available resources(!) or even less. Note that a typical server has 4 CPU cores or more, naive deployment of Node.js utilizes only 1 (even using PaaS services like AWS beanstalk!)
🔗 [Read More: Utilize all CPU cores](./sections/production/utilizecpu.md)
![✔] 5.7. Create a ‘maintenance endpoint’
TL;DR: Expose a set of system-related information, like memory usage and REPL, etc in a secured API. Although it’s highly recommended to rely on standard and battle-tested tools, some valuable information and operations are easier done using code
Otherwise: You’ll find that you’re performing many “diagnostic deploys” – shipping code to production only to extract some information for diagnostic purposes
🔗 [Read More: Create a ‘maintenance endpoint’](./sections/production/createmaintenanceendpoint.md)
![✔] 5.8. Discover errors and downtime using APM products
TL;DR: Application monitoring and performance products (a.k.a. APM) proactively gauge codebase and API so they can auto-magically go beyond traditional monitoring and measure the overall user-experience across services and tiers. For example, some APM products can highlight a transaction that loads too slow on the end-user's side while suggesting the root cause
Otherwise: You might spend great effort on measuring API performance and downtimes, probably you’ll never be aware which is your slowest code parts under real-world scenario and how these affect the UX
🔗 [Read More: Discover errors and downtime using APM products](./sections/production/apmproducts.md)
![✔] 5.9. Make your code production-ready
TL;DR: Code with the end in mind, plan for production from day 1. This sounds a bit vague so I’ve compiled a few development tips that are closely related to production maintenance (click Gist below)
Otherwise: A world champion IT/DevOps guy won’t save a system that is badly written
🔗 [Read More: Make your code production-ready](./sections/production/productioncode.md)
![✔] 5.10. Measure and guard the memory usage
TL;DR: Node.js has controversial relationships with memory: the v8 engine has soft limits on memory usage (1.4GB) and there are known paths to leak memory in Node’s code – thus watching Node’s process memory is a must. In small apps, you may gauge memory periodically using shell commands but in medium-large apps consider baking your memory watch into a robust monitoring system
Otherwise: Your process memory might leak a hundred megabytes a day like how it happened at Walmart
🔗 [Read More: Measure and guard the memory usage](./sections/production/measurememory.md)
![✔] 5.11. Get your frontend assets out of Node
TL;DR: Serve frontend content using dedicated middleware (nginx, S3, CDN) because Node performance really gets hurt when dealing with many static files due to its single-threaded model
Otherwise: Your single Node thread will be busy streaming hundreds of html/images/angular/react files instead of allocating all its resources for the task it was born for – serving dynamic content
🔗 [Read More: Get your frontend assets out of Node](./sections/production/frontendout.md)
![✔] 5.12. Be stateless, kill your servers almost every day
TL;DR: Store any type of data (e.g. user sessions, cache, uploaded files) within external data stores. Consider ‘killing’ your servers periodically or use ‘serverless’ platform (e.g. AWS Lambda) that explicitly enforces a stateless behavior
Otherwise: Failure at a given server will result in application downtime instead of just killing a faulty machine. Moreover, scaling-out elasticity will get more challenging due to the reliance on a specific server
🔗 [Read More: Be stateless, kill your Servers almost every day](./sections/production/bestateless.md)
![✔] 5.13. Use tools that automatically detect vulnerabilities
TL;DR: Even the most reputable dependencies such as Express have known vulnerabilities (from time to time) that can put a system at risk. This can be easily tamed using community and commercial tools that constantly check for vulnerabilities and warn (locally or at GitHub), some can even patch them immediately
Otherwise: Keeping your code clean from vulnerabilities without dedicated tools will require you to constantly follow online publications about new threats. Quite tedious
🔗 [Read More: Use tools that automatically detect vulnerabilities](./sections/production/detectvulnerabilities.md)
![✔] 5.14. Assign a transaction id to each log statement
Also known as correlation id / transit id / tracing id / request id / request context / etc.
TL;DR: Assign the same identifier, transaction-id: {some value}, to each log entry within a single request. Then when inspecting errors in logs, easily conclude what happened before and after. Until version 14 of Node, this was not easy to achieve due to Node's async nature, but since AsyncLocalStorage came to town, this became possible and easy than ever. see code examples inside
Otherwise: Looking at a production error log without the context – what happened before – makes it much harder and slower to reason about the issue
🔗 [Read More: Assign ‘TransactionId’ to each log statement](./sections/production/assigntransactionid.md)
![✔] 5.15. Set NODE_ENV=production
TL;DR: Set the environment variable NODE_ENV to ‘production’ or ‘development’ to flag whether production optimizations should get activated – many npm packages determine the current environment and optimize their code for production
Otherwise: Omitting this simple property might greatly degrade performance. For example, when using Express for server-side rendering omitting NODE_ENV makes it slower by a factor of three!
🔗 [Read More: Set NODE_ENV=production](./sections/production/setnodeenv.md)
![✔] 5.16. Design automated, atomic and zero-downtime deployments
TL;DR: Research shows that teams who perform many deployments lower the probability of severe production issues. Fast and automated deployments that don’t require risky manual steps and service downtime significantly improve the deployment process. You should probably achieve this using Docker combined with CI tools as they became the industry standard for streamlined deployment
Otherwise: Long deployments -> production downtime & human-related error -> team unconfident in making deployment -> fewer deployments and features
![✔] 5.17. Use an LTS release of Node.js
TL;DR: Ensure you are using an LTS version of Node.js to receive critical bug fixes, security updates and performance improvements
Otherwise: Newly discovered bugs or vulnerabilities could be used to exploit an application running in production, and your application may become unsupported by various modules and harder to maintain
🔗 [Read More: Use an LTS release of Node.js](./sections/production/LTSrelease.md)
![✔] 5.18. Don't route logs within the app
TL;DR: Log destinations should not be hard-coded by developers within the application code, but instead should be defined by the execution environment the application runs in. Developers should write logs to `