mirror of
https://github.com/goldbergyoni/nodebestpractices.git
synced 2026-03-13 08:40:16 +08:00
Merge branch 'goldbergyoni:master' into hebrew-translation-2023
This commit is contained in:
94
README.md
94
README.md
@@ -9,7 +9,7 @@
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/⚙%20Item%20count%20-%20102%20Best%20Practices-blue.svg" alt="102 items"/> <img id="last-update-badge" src="https://img.shields.io/badge/%F0%9F%93%85%20Last%20update%20-%20April%2019%2C%202023-green.svg" alt="Last update: April 19, 2023" /> <img src="https://img.shields.io/badge/ %E2%9C%94%20Updated%20For%20Version%20-%20Node%2014.0.0-brightgreen.svg" alt="Updated for Node 14.0.0"/>
|
||||
<img src="https://img.shields.io/badge/⚙%20Item%20count%20-%20102%20Best%20Practices-blue.svg" alt="102 items"/> <img id="last-update-badge" src="https://img.shields.io/badge/%F0%9F%93%85%20Last%20update%20-%20July%2019%2C%202023-green.svg" alt="Last update: July 19, 2023" /> <img src="https://img.shields.io/badge/ %E2%9C%94%20Updated%20For%20Version%20-%20Node%2019.0.0-brightgreen.svg" alt="Updated for Node 19.0.0"/>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
@@ -22,19 +22,13 @@ Read in a different language: [**CN**](./README.chin
|
||||
|
||||
<br/>
|
||||
|
||||
## 🚀 We have an [official Node.js starter - Practica.js](https://github.com/practicajs/practica). Use it to generate a new solution skeleton with all the practices baked inside. Or just it to learn by code examples
|
||||
# 🎊 2023 edition is here!
|
||||
|
||||
<br/>
|
||||
- **🛰 Modernized to 2023**: Tons of text edits, new recommended libraries, and some new best practices
|
||||
|
||||
# Latest Best Practices and News
|
||||
- **✨ Easily focus on new content**: Already visited before? Search for `#new` or `#updated` tags for new content only
|
||||
|
||||
- **🛰 2023 edition is released soon**: We're now writing the next edition, stay tuned?
|
||||
|
||||
- **✨ 89,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](https://github.com/rubek-joshi)
|
||||
|
||||
- ** French translation!1! :** The latest translation that joins our international guide is French. Bienvenue
|
||||
- **🔖 Curious to see examples? We have a starter**: Visit [Practica.js](https://github.com/practicajs/practica), our application example and boilerplate (beta) to see some practices in action
|
||||
|
||||
<br/><br/>
|
||||
|
||||
@@ -48,6 +42,12 @@ Read in a different language: [**CN**](./README.chin
|
||||
|
||||
<br/><br/>
|
||||
|
||||
# By Yoni Goldberg
|
||||
|
||||
### Learn with me: As a consultant, I engage with worldwide teams on various activities like workshops and code reviews. 🎉AND... Hold on, I've just launched my [beyond-the-basics testing course, which is on a 🎁 limited-time sale](https://testjavascript.com/) until August 7th
|
||||
|
||||
<br/><br/>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<details>
|
||||
@@ -73,7 +73,7 @@ Read in a different language: [**CN**](./README.chin
|
||||
  [2.2 Extend the built-in Error object `#strategic` `#updated`](#-22-extend-the-built-in-error-object)</br>
|
||||
  [2.3 Distinguish operational vs programmer errors `#strategic` `#updated`](#-23-distinguish-catastrophic-errors-from-operational-errors)</br>
|
||||
  [2.4 Handle errors centrally, not within a middleware `#strategic`](#-24-handle-errors-centrally-not-within-a-middleware)</br>
|
||||
  [2.5 Document API errors using OpenAPI or GraphQL](#-25-document-api-errors-using-openapi-or -graphql)</br>
|
||||
  [2.5 Document API errors using OpenAPI or GraphQL](#-25-document-api-errors-using-openapi-or-graphql)</br>
|
||||
  [2.6 Exit the process gracefully when a stranger comes to town `#strategic`](#-26-exit-the-process-gracefully-when-a-stranger-comes-to-town)</br>
|
||||
  [2.7 Use a mature logger to increase errors visibility `#updated`](#-27-use-a-mature-logger-to-increase-errors-visibility)</br>
|
||||
  [2.8 Test error flows using your favorite test framework `#updated`](#-28-test-error-flows-using-your-favorite-test-framework)</br>
|
||||
@@ -81,6 +81,7 @@ Read in a different language: [**CN**](./README.chin
|
||||
  [2.10 Catch unhandled promise rejections `#updated`](#-210-catch-unhandled-promise-rejections)</br>
|
||||
  [2.11 Fail fast, validate arguments using a dedicated library](#-211-fail-fast-validate-arguments-using-a-dedicated-library)</br>
|
||||
  [2.12 Always await promises before returning to avoid a partial stacktrace `#new`](#-212-always-await-promises-before-returning-to-avoid-a-partial-stacktrace)</br>
|
||||
  [2.13 Subscribe to event emitters 'error' event `#new`](#-213-subscribe-to-event-emitters-and-streams-error-event)</br>
|
||||
|
||||
</details>
|
||||
|
||||
@@ -148,7 +149,7 @@ Read in a different language: [**CN**](./README.chin
|
||||
  [5.15. Set NODE_ENV=production](#-515-set-node_envproduction)</br>
|
||||
  [5.16. Design automated, atomic and zero-downtime deployments `#advanced`](#-516-design-automated-atomic-and-zero-downtime-deployments)</br>
|
||||
  [5.17. Use an LTS release of Node.js](#-517-use-an-lts-release-of-nodejs)</br>
|
||||
  [5.18. Log to stdout, avoid specifying log destination within the app](#-518-log-to-stdout-avoid-specifying-log-destination-within-the-app)</br>
|
||||
  [5.18. Log to stdout, avoid specifying log destination within the app `#updated`](#-518-log-to-stdout-avoid-specifying-log-destination-within-the-app)</br>
|
||||
  [5.19. Install your packages with npm ci `#new`](#-519-install-your-packages-with-npm-ci)</br>
|
||||
|
||||
</details>
|
||||
@@ -227,6 +228,8 @@ Read in a different language: [**CN**](./README.chin
|
||||
|
||||
## ![✔] 1.1 Structure your solution by business components
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** The root of a system should contain folders or repositories that represent reasonably sized business modules. Each component represents a product domain (i.e., bounded context), like 'user-component', 'order-component', etc. Each component has its own API, logic, and logical database. What is the significant merit? With an autonomous component, every change is performed over a granular and smaller scope - the mental overload, development friction, and deployment fear are much smaller and better. As a result, developers can move much faster. This does not necessarily demand physical separation and can be achieved using a Monorepo or with a multi-repo
|
||||
|
||||
```bash
|
||||
@@ -248,6 +251,8 @@ my-system
|
||||
|
||||
## ![✔] 1.2 Layer your components with 3-tiers, keep the web layer within its boundaries
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** Each component should contain 'layers' - a dedicated folder for common concerns: 'entry-point' where controller lives, 'domain' where the logic lives, and 'data-access'. The primary principle of the most popular architectures is to separate the technical concerns (e.g., HTTP, DB, etc) from the pure logic of the app so a developer can code more features without worrying about infrastructural concerns. Putting each concern in a dedicated folder, also known as the [3-Tier pattern](https://en.wikipedia.org/wiki/Multitier_architecture), is the _simplest_ way to meet this goal
|
||||
|
||||
```bash
|
||||
@@ -291,7 +296,9 @@ my-system
|
||||
|
||||
## ![✔] 1.4 Use environment aware, secure and hierarchical config
|
||||
|
||||
**TL;DR:** A 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 (d) typing support (e) validation for failing fast (f) Specify default for each key. There are a few packages that can help tick most of those boxes like [convict](https://www.npmjs.com/package/convict), [env-var](env-var), [zod](https://github.com/colinhacks/zod), and others
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** A 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 (d) typing support (e) validation for failing fast (f) Specify default for each key. There are a few packages that can help tick most of those boxes like [convict](https://www.npmjs.com/package/convict), [env-var](https://github.com/evanshortiss/env-var), [zod](https://github.com/colinhacks/zod), and others
|
||||
|
||||
**Otherwise:** Consider a mandatory environment variable that wasn't provided. The app starts successfully and serve requests, some information is already persisted to DB. Then, it's realized that without this mandatory key the request can't complete, leaving the app in a dirty state
|
||||
|
||||
@@ -301,6 +308,8 @@ my-system
|
||||
|
||||
## ![✔] 1.5 Consider all the consequences when choosing the main framework
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** When building apps and APIs, using a framework is mandatory. It's easy to overlook alternative frameworks or important considerations and then finally land on a sub optimal option. As of 2023/2024, we believe that these four frameworks are worth considering: [Nest.js](https://nestjs.com/), [Fastify](https://www.fastify.io/), [express](https://expressjs.com/), and [Koa](https://koajs.com/). Click read more below for a detailed pros/cons of each framework. Simplistically, we believe that Nest.js is the best match for teams who wish to go OOP and/or build large-scale apps that can't get partitioned into smaller _autonomous_ components. Fastify is our recommendation for apps with reasonably-sized components (e.g., Microservices) that are built around simple Node.js mechanics. Read our [full considerations guide here](./sections/projectstructre/choose-framework.md)
|
||||
|
||||
**Otherwise:** Due to the overwhelming amount of considerations, it's easy to make decisions based on partial information and compare apples with oranges. For example, it's believed that Fastify is a minimal web-server that should get compared with express only. In reality, it's a rich framework with many official plugins that cover many concerns
|
||||
@@ -309,6 +318,8 @@ my-system
|
||||
|
||||
## ![✔] 1.6 Use TypeScript sparingly and thoughtfully
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** Coding without type safety is no longer an option, TypeScript is the most popular option for this mission. Use it to define variables and functions return types. With that, it is also a double edge sword that can greatly _encourage_ complexity with its additional ~ 50 keywords and sophisticated features. Consider using it sparingly, mostly with simple types, and utilize advanced features only when a real need arises
|
||||
|
||||
**Otherwise:** [Researches](https://earlbarr.com/publications/typestudy.pdf) show that using TypeScript can help in detecting ~20% bugs earlier. Without it, also the developer experience in the IDE is intolerable. On the flip side, 80% of other bugs were not discovered using types. Consequently, typed syntax is valuable but limited. Only efficient tests can discover the whole spectrum of bugs, including type-related bugs. It might also defeat its purpose: sophisticated code features are likely to increase the code complexity, which by itself increases both the amount of bugs and the average bug fix time
|
||||
@@ -333,6 +344,8 @@ my-system
|
||||
|
||||
## ![✔] 2.2 Extend the built-in Error object
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** Some libraries throw errors as a string or as some custom type – this complicates the error handling logic and the interoperability between modules. Instead, create app error object/class that extends the built-in Error object and use it whenever rejecting, throwing or emitting an error. The app error should add useful imperative properties like the error name/code and isCatastrophic. By doing so, all errors have a unified structure and support better error handling .There is `no-throw-literal` ESLint rule that strictly checks that (although it has some [limitations](https://eslint.org/docs/rules/no-throw-literal) 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!
|
||||
@@ -343,6 +356,8 @@ my-system
|
||||
|
||||
## ![✔] 2.3 Distinguish catastrophic errors from operational errors
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**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, catastrophic error (also known as programmer errors) refers to unusual 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 catastrophic 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
|
||||
@@ -383,6 +398,8 @@ my-system
|
||||
|
||||
## ![✔] 2.7 Use a mature logger to increase errors visibility
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** A robust logging tools like [Pino](https://github.com/pinojs/pino) or [Winston](https://github.com/winstonjs/winston) increases the errors visibility using features like log-levels, pretty print coloring and more. Console.log lacks these imperative features and should be avoided. The best in class logger allows attaching custom useful properties to log entries with minimized serialization performance penalty. Developers should write logs to `stdout` and let the infrastructure pipe the stream to the appropriate log aggregator
|
||||
|
||||
**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
|
||||
@@ -393,6 +410,8 @@ my-system
|
||||
|
||||
## ![✔] 2.8 Test error flows using your favorite test framework
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**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. On top of this, simulate deeper error flows like uncaught exceptions an ensure that the error handler treat these properly (see code examples within the "read more" section)
|
||||
|
||||
**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
|
||||
@@ -413,6 +432,8 @@ my-system
|
||||
|
||||
## ![✔] 2.10 Catch unhandled promise rejections
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**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
|
||||
@@ -433,6 +454,8 @@ my-system
|
||||
|
||||
## ![✔] 2.12 Always await promises before returning to avoid a partial stacktrace
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**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
|
||||
@@ -443,6 +466,16 @@ especially if the cause of the abnormal behavior is inside of the missing functi
|
||||
|
||||
🔗 [**Read More: returning promises**](./sections/errorhandling/returningpromises.md)
|
||||
|
||||
<br/><br/>
|
||||
|
||||
## ![✔] 2.13 Subscribe to event emitters and streams 'error' event
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** Unlike typical functions, a try-catch clause won't get errors that originate from Event Emitters and anything inherited from it (e.g., streams). Instead of try-catch, subscribe to an event emitter's 'error' event so your code can handle the error in context. When dealing with [EventTargets](https://nodejs.org/api/events.html#eventtarget-and-event-api) (the web standard version of Event Emitters) there are no 'error' event and all errors end in the process.on('error) global event - in this case, at least ensure that the process crash or not based on the desired context. Also, mind that error originating from _asynchronous_ event handlers are not get caught unless the event emitter is initialized with {captureRejections: true}
|
||||
|
||||
**Otherwise:** Event emitters are commonly used for global and key application functionality such as DB or message queue connection. When this kind of crucial objects throw an error, at best the process will crash due to unhandled exception. Even worst, it will stay alive as a zombie while a key functionality is turned off
|
||||
|
||||
<br/><br/><br/>
|
||||
|
||||
<p align="right"><a href="#table-of-contents">⬆ Return to top</a></p>
|
||||
@@ -461,6 +494,8 @@ especially if the cause of the abnormal behavior is inside of the missing functi
|
||||
|
||||
## ![✔] 3.2 Use Node.js eslint extension plugins
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** On top of ESLint standard rules that cover vanilla JavaScript, add Node.js specific plugins like [eslint-plugin-node](https://www.npmjs.com/package/eslint-plugin-node), [eslint-plugin-mocha](https://www.npmjs.com/package/eslint-plugin-mocha) and [eslint-plugin-node-security](https://www.npmjs.com/package/eslint-plugin-security), [eslint-plugin-require](https://www.npmjs.com/package/eslint-plugin-require), [/eslint-plugin-jest](https://www.npmjs.com/package/eslint-plugin-jest) and other useful rules
|
||||
|
||||
**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
|
||||
@@ -480,7 +515,8 @@ function someFunction() {
|
||||
}
|
||||
|
||||
// Avoid
|
||||
function someFunction() {
|
||||
function someFunction()
|
||||
{
|
||||
// code block
|
||||
}
|
||||
```
|
||||
@@ -604,6 +640,8 @@ function doSomething() {
|
||||
|
||||
## ![✔] 3.9 Set an explicit entry point to a module/folder
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** When developing a module/library, set an explicit root file that exports the public and interesting code. Discourage the client code from importing deep files and becoming familiar with the internal structure. With commonjs (require), this can be done with an index.js file at the folder's root or the package.json.main field. With ESM (import), if a package.json exists on the root, the field "exports" allow specifying the module's root file. If no package.json exists, you may put an index.js file on the root which re-exports all the public functionality
|
||||
|
||||
**Otherwise:** Having an explicit root file acts like a public 'interface' that encapsulates the internal, directs the caller to the public code and facilitates future changes without breaking the contract
|
||||
@@ -656,7 +694,7 @@ 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
|
||||
**TL;DR:** Async-await is the simplest way to express an asynchronous flow as it makes asynchronous code look synchronous. Async-await will also result in much more compact code and support for try-catch. This technique now supersedes callbacks and promises in _most_ cases. Using it in your code is probably the best gift one can give to the code reader
|
||||
|
||||
**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
|
||||
|
||||
@@ -676,6 +714,8 @@ All statements above will return false if used with `===`
|
||||
|
||||
## ![✔] 3.13 Avoid effects outside of functions
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** Avoid putting code with effects like network or DB calls outside of functions. Such a code will be executed immediately when another file requires the file. This 'floating' code might get executed when the underlying system is not ready yet. It also comes with a performance penalty even when this module's functions will finally not be used in runtime. Last, mocking these DB/network calls for testing is harder outside of functions. Instead, put this code inside functions that should get called explicitly. If some DB/network code must get executed right when the module loads, consider using the factory or revealing module patterns
|
||||
|
||||
**Otherwise:** A typical web framework sets error handler, environment variables and monitoring. When DB/network calls are made before the web framework is initialized, they won't be monitored or fail due to a lack of configuration data
|
||||
@@ -702,6 +742,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 4.2 Include 3 parts in each test name
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**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?
|
||||
@@ -712,6 +754,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 4.3 Structure tests by the AAA pattern
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**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
|
||||
@@ -722,6 +766,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 4.4 Ensure Node version is unified
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** Use tools that encourage or enforce the same Node.js version across different environments and developers. Tools like [nvm](https://github.com/nvm-sh/nvm), and [Volta](https://volta.sh/) allow specifying the project's version in a file so each team member can run a single command to conform with the project's version. Optionally, this definition can be replicated to CI and the production runtime (e.g., copy the specified value to .Dockerfile build and to the CI declaration file)
|
||||
|
||||
**Otherwise:** A developer might face or miss an error because she uses a different Node.js version than her teammates. Even worse - the production runtime might be different than the environment where tests were executed
|
||||
@@ -774,6 +820,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 4.10 Mock responses of external HTTP services
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** Use network mocking tools to simulate responses of external collaborators' services that are approached over the network (e.g., REST, Graph). This is imperative not only to isolate the component under test but mostly to simulate non-happy path flows. Tools like [nock](https://github.com/nock/nock) (in-process) or [Mock-Server](https://www.mock-server.com/) allow defining a specific response of external service in a single line of code. Remember to simulate also errors, delays, timeouts, and any other event that is likely to happen in production
|
||||
|
||||
**Otherwise:** Allowing your component to reach real external services instances will likely result in naive tests that mostly cover happy paths. The tests might also be flaky and slow
|
||||
@@ -790,6 +838,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 4.12 Specify a port in production, randomize in testing
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** When testing against the API, it's common and desirable to initialize the web server inside the tests. Let the server randomize the web server port in testing to prevent collisions. If you're using Node.js http server (used by most frameworks), doing so demands nothing but passing a port number zero - this will randomize an available port
|
||||
|
||||
**Otherwise:** Specifying a fixed port will prevent two testing processes from running at the same time. Most of the modern test runners run with multiple processes by default
|
||||
@@ -798,6 +848,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 4.13 Test the five possible outcomes
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** When testing a flow, ensure to cover five potential categories. Any time some action is triggered (e.g., API call), a reaction occurs, a meaningful **outcome** is produced and calls for testing. There are five possible outcome types for every flow: a response, a visible state change (e.g., DB), an outgoing API call, a new message in a queue, and an observability call (e.g., logging, metric). See a [checklist here](https://testjavascript.com/wp-content/uploads/2021/10/the-backend-checklist.pdf). Each type of outcome comes with unique challenges and techniques to mitigate those challenges - we have a dedicated guide about this topic: [Node.js testing - beyond the basics](https://github.com/testjavascript/nodejs-integration-tests-best-practices)
|
||||
|
||||
**Otherwise:** Consider a case when testing the addition of a new product to the system. It's common to see tests that assert on a valid response only. What if the product was failed to persist regardless of the positive response? what if when adding a new product demands calling some external service, or putting a message in the queue - shouldn't the test assert these outcomes as well? It's easy to overlook various paths, this is where a [checklist comes handy](https://testjavascript.com/wp-content/uploads/2021/10/the-backend-checklist.pdf)
|
||||
@@ -882,6 +934,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 5.8. Discover the unknowns using APM products
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**TL;DR:** Consider adding another safety layer to the production stack - APM. While the majority of symptoms and causes can be detected using traditional monitoring techniques, in a distributed system there is more than meets the eye. Application monitoring and performance products (a.k.a. APM) can auto-magically go beyond traditional monitoring and provide additional layer of discovery and developer-experience. For example, some APM products can highlight a transaction that loads too slow on the **end-user's side** while suggesting the root cause. APMs also provide more context for developers who try to troubleshoot a log error by showing what was the server busy with when the error occurred. To name a few example
|
||||
|
||||
**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
|
||||
@@ -980,6 +1034,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 5.18. Log to stdout, avoid specifying log destination within the app
|
||||
|
||||
### `📝 #updated`
|
||||
|
||||
**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 `stdout` using a logger utility and then let the execution environment (container, server, etc.) pipe the `stdout` stream to the appropriate destination (i.e. Splunk, Graylog, ElasticSearch, etc.).
|
||||
|
||||
**Otherwise:** If developers set the log routing, less flexibility is left for the ops professional who wishes to customize it. Beyond this, if the app tries to log directly to a remote location (e.g., Elastic Search), in case of panic or crash - further logs that might explain the problem won't arrive
|
||||
@@ -1311,6 +1367,8 @@ b. [Node.js testing - beyond the basics](https://github.com/testjavascript/nodej
|
||||
|
||||
## ![✔] 6.27. Import built-in modules using the 'node:' protocol
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
<a href="https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/" target="_blank"><img src="https://img.shields.io/badge/%E2%9C%94%20A06:2021 – Vulnerable and Outdated Components-green.svg" alt=""/></a>
|
||||
|
||||
**TL;DR:** Import or require built-in Node.js modules using the 'node protocol' syntax:
|
||||
@@ -1494,6 +1552,8 @@ In addition, referring to an image tag means that the base image is subject to c
|
||||
|
||||
## ![✔] 8.11. Clean-out build-time secrets, avoid secrets in args
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** Avoid secrets leaking from the Docker build environment. A Docker image is typically shared in multiple environment like CI and a registry that are not as sanitized as production. A typical example is an npm token which is usually passed to a dockerfile as argument. This token stays within the image long after it is needed and allows the attacker indefinite access to a private npm registry. This can be avoided by coping a secret file like `.npmrc` and then removing it using multi-stage build (beware, build history should be deleted as well) or by using Docker build-kit secret feature which leaves zero traces
|
||||
|
||||
**Otherwise:** Everyone with access to the CI and docker registry will also get access to some precious organization secrets as a bonus
|
||||
@@ -1532,6 +1592,8 @@ In addition, referring to an image tag means that the base image is subject to c
|
||||
|
||||
## ![✔] 8.15. Lint your Dockerfile
|
||||
|
||||
### `🌟 #new`
|
||||
|
||||
**TL;DR:** Linting your Dockerfile is an important step to identify issues in your Dockerfile which differ from best practices. By checking for potential flaws using a specialised Docker linter, performance and security improvements can be easily identified, saving countless hours of wasted time or security issues in production code.
|
||||
|
||||
**Otherwise:** Mistakenly the Dockerfile creator left Root as the production user, and also used an image from unknown source repository. This could be avoided with with just a simple linter.
|
||||
|
||||
@@ -256,7 +256,8 @@ function someFunction() {
|
||||
}
|
||||
|
||||
// Avoid
|
||||
function someFunction() {
|
||||
function someFunction()
|
||||
{
|
||||
// code block
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,7 +25,7 @@ Welcome to the biggest compilation of Node.js best practices, based on our check
|
||||
|
||||
##  1. Structure your solution by feature ('microservices')
|
||||
|
||||
**TL&DR:** The worst large applications pitfal is a huge code base where hundreds of dependencies slow down developers as try to incorporate new features. Partioning into small units ensures that each unit is kept simple and very easy to maintain. This strategy pushes the complexity to the higher level - designing the cross-component interactions.
|
||||
**TL&DR:** The worst large applications pitfal is a huge code base where hundreds of dependencies slow down developers as try to incorporate new features. Partitioning into small units ensures that each unit is kept simple and very easy to maintain. This strategy pushes the complexity to the higher level - designing the cross-component interactions.
|
||||
|
||||
**Otherwise:** Developing a new feature with a change to few objects demands to evaluate how this changes might affect dozends of dependants and ach deployment becomes a fear.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
### One Paragraph Explainer
|
||||
|
||||
When an error occurs, whether from a synchronous or asynchronous flow, it's imperative to have a full stacktrace of the error flow. Surprisingly, if an async function returns a promise (e.g. calls other async function) without awaiting, should an error occur then the caller function won't appear in the stacktrace. This will leave the person who diagnoses the error with partial information - All the more if the error cause lies within that caller function. There is a feature v8 called "zero-cost async stacktraces" that allow stacktraces not to be cut on the most recent `await`. But due to non-trivial implementation details, it will not work if the return value of a function (sync or async) is a promise. So, to avoid holes in stacktraces when returned promises would be rejected, we must always explicitly resolve promises with `await` before returning them from functions
|
||||
When an error occurs, whether from a synchronous or asynchronous flow, it's imperative to have a full stacktrace of the error flow. Surprisingly, if an async function returns a promise (e.g. calls other async function) without awaiting, should an error occur then the caller function won't appear in the stacktrace. This will leave the person who diagnoses the error with partial information - All the more if the error cause lies within that caller function. There is a v8 feature called "zero-cost async stacktraces" that allows stacktraces to not be cut on the most recent `await`. But due to non-trivial implementation details, it will not work if the return value of a function (sync or async) is a promise. So, to avoid holes in stacktraces when returned promises would be rejected, we must always explicitly resolve promises with `await` before returning them from functions
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -87,7 +87,7 @@ async function asyncFn () {
|
||||
return await syncFn()
|
||||
}
|
||||
|
||||
// 👎 syncFn would be missing in the stacktrace because it returns a promise while been sync
|
||||
// 👎 syncFn would be missing in the stacktrace because it returns a promise while being sync
|
||||
asyncFn().catch(console.log)
|
||||
```
|
||||
|
||||
@@ -165,7 +165,7 @@ Error: stacktrace is missing the place where getUser has been called
|
||||
at async Promise.all (index 2)
|
||||
```
|
||||
|
||||
*Side-note*: it may looks like `Promise.all (index 2)` can help understanding the place where `getUser` has been called,
|
||||
*Side-note*: it may look like `Promise.all (index 2)` can help understanding the place where `getUser` has been called,
|
||||
but due to a [completely different bug in v8](https://bugs.chromium.org/p/v8/issues/detail?id=9023), `(index 2)` is
|
||||
a line from internals of v8
|
||||
|
||||
@@ -177,9 +177,9 @@ a line from internals of v8
|
||||
<details><summary>Javascript</summary>
|
||||
<p>
|
||||
|
||||
*Note 1*: in case if you control the code of the function that would call the callback - just change that function to
|
||||
async and add `await` before the callback call. Below I assume that you are not in charge of the code that is calling
|
||||
the callback (or it's change is unacceptable for example because of backward compatibility)
|
||||
*Note 1*: if you control the code of the function that would call the callback - just change that function to
|
||||
`async` and add `await` before the callback call. Below I assume that you are not in charge of the code that is calling
|
||||
the callback (or its change is unacceptable for example because of backward compatibility)
|
||||
|
||||
*Note 2*: quite often usage of async callback in places where sync one is expected would not work at all. This is not about
|
||||
how to fix the code that is not working - it's about how to fix stacktrace in case if code is already working as
|
||||
@@ -210,8 +210,8 @@ Error: with all frames present
|
||||
where thanks to explicit `await` in `map`, the end of the line `at async ([...])` would point to the exact place where
|
||||
`getUser` has been called
|
||||
|
||||
*Side-note*: if async function that wrap `getUser` would miss `await` before return (anti-pattern #1 + anti-pattern #3)
|
||||
then only one frame would left in the stacktrace:
|
||||
*Side-note*: if async function that wrap `getUser` lacks `await` before return (anti-pattern #1 + anti-pattern #3)
|
||||
then only one frame would be left in the stacktrace:
|
||||
|
||||
```javascript
|
||||
[...]
|
||||
@@ -235,12 +235,12 @@ Error: [...]
|
||||
## Advanced explanation
|
||||
|
||||
The mechanisms behind sync functions stacktraces and async functions stacktraces in v8 implementation are quite different:
|
||||
sync stacktrace is based on **stack** provided by operating system Node.js is running on (just like in most programming
|
||||
languages). When an async function is executing, the **stack** of operating system is popping it out as soon as the
|
||||
function is getting to it's first `await`. So async stacktrace is a mix of operating system **stack** and a rejected
|
||||
**promise resolution chain**. Zero-cost async stacktraces implementation is extending the **promise resolution chain**
|
||||
sync stacktrace is based on **stack** provided by the operating system Node.js is running on (just like in most programming
|
||||
languages). When an async function is executing, the **stack** of the operating system is popping it out as soon as the
|
||||
function gets to its first `await`. So async stacktrace is a mix of operating system **stack** and a rejected
|
||||
**promise resolution chain**. Zero-cost async stacktraces implementation extends the **promise resolution chain**
|
||||
only when the promise is getting `awaited` <span>[¹](#1)</span>. Because only `async` functions may `await`,
|
||||
sync function would always be missed in async stacktrace if any async operation has been performed after the function
|
||||
sync function would always be missing from async stacktrace if any async operation has been performed after the function
|
||||
has been called <span>[²](#2)</span>
|
||||
|
||||
### The tradeoff
|
||||
@@ -256,13 +256,13 @@ definitely should never be done up-front
|
||||
|
||||
### Why return await was considered as anti-pattern in the past
|
||||
|
||||
There is a number of [excellent articles](https://jakearchibald.com/2017/await-vs-return-vs-return-await/) explained
|
||||
There is a number of [excellent articles](https://jakearchibald.com/2017/await-vs-return-vs-return-await/) explaining
|
||||
why `return await` should never be used outside of `try` block and even an
|
||||
[ESLint rule](https://eslint.org/docs/rules/no-return-await) that disallows it. The reason for that is the fact that
|
||||
since async/await become available with transpilers in Node.js 0.10 (and got native support in Node.js 7.6) and until
|
||||
"zero-cost async stacktraces" was introduced in Node.js 10 and unflagged in Node.js 12, `return await` was absolutely
|
||||
equivalent to `return` for any code outside of `try` block. It may still be the same for some other ES engines. This
|
||||
is why resolving promises before returning them is the best practice for Node.js and not for the EcmaScript in general
|
||||
is why resolving promises before returning them is the best practice for Node.js and not for ECMAScript in general
|
||||
|
||||
### Notes:
|
||||
|
||||
@@ -270,7 +270,7 @@ is why resolving promises before returning them is the best practice for Node.js
|
||||
must always be built synchronously, on the same tick of event loop <span id="a1">[¹](#1)</span>
|
||||
2. Without `await` in `throwAsync` the code would be executed in the same phase of event loop. This is a
|
||||
degenerated case when OS **stack** would not get empty and stacktrace be full even without explicitly
|
||||
awaiting the function result. Usually usage of promises include some async operations and so parts of
|
||||
awaiting the function result. Common usage of promises includes some async operations and so parts of
|
||||
the stacktrace would get lost
|
||||
3. Zero-cost async stacktraces still would not work for complicated promise usages e.g. single promise
|
||||
awaited many times in different places
|
||||
|
||||
Reference in New Issue
Block a user