diff --git a/content/cli/overview.md b/content/cli/overview.md index fe383dba03..71644baea1 100644 --- a/content/cli/overview.md +++ b/content/cli/overview.md @@ -48,15 +48,15 @@ Aside from a few specific considerations around how the **build** process works You can use either mode to manage multiple projects. Here's a quick summary of the differences: -| Feature | Standard Mode | Monorepo Mode | -| ----------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | -| Multiple projects | Separate file system structure | Single file system structure | -| `node_modules` & `package.json` | Separate instances | Shared across monorepo | -| Default compiler | `tsc` | webpack | -| Compiler settings | Specified separately | Monorepo defaults that can be overridden per project | -| Config files like `.eslintrc.js`, `.prettierrc`, etc. | Specified separately | Shared across monorepo | -| `nest build` and `nest start` commands | Target defaults automatically to the (only) project in the context | Target defaults to the **default project** in the monorepo | -| Libraries | Managed manually, usually via npm packaging | Built-in support, including path management and bundling | +| Feature | Standard Mode | Monorepo Mode | +| ---------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | +| Multiple projects | Separate file system structure | Single file system structure | +| `node_modules` & `package.json` | Separate instances | Shared across monorepo | +| Default compiler | `tsc` | webpack | +| Compiler settings | Specified separately | Monorepo defaults that can be overridden per project | +| Config files like `eslint.config.mjs`, `.prettierrc`, etc. | Specified separately | Shared across monorepo | +| `nest build` and `nest start` commands | Target defaults automatically to the (only) project in the context | Target defaults to the **default project** in the monorepo | +| Libraries | Managed manually, usually via npm packaging | Built-in support, including path management and bundling | Read the sections on [Workspaces](/cli/monorepo) and [Libraries](/cli/libraries) for more detailed information to help you decide which mode is most suitable for you. diff --git a/content/cli/usages.md b/content/cli/usages.md index 64ad2b00e1..4811dc662f 100644 --- a/content/cli/usages.md +++ b/content/cli/usages.md @@ -54,25 +54,25 @@ $ nest g [options] ##### Schematics -| Name | Alias | Description | -| ------------- | ----- | ----------------------------------------------------------------------------------------------------------------------| -| `app` | | Generate a new application within a monorepo (converting to monorepo if it's a standard structure). | -| `library` | `lib` | Generate a new library within a monorepo (converting to monorepo if it's a standard structure). | -| `class` | `cl` | Generate a new class. | -| `controller` | `co` | Generate a controller declaration. | -| `decorator` | `d` | Generate a custom decorator. | -| `filter` | `f` | Generate a filter declaration. | -| `gateway` | `ga` | Generate a gateway declaration. | -| `guard` | `gu` | Generate a guard declaration. | -| `interface` | `itf` | Generate an interface. | -| `interceptor` | `itc` | Generate an interceptor declaration. | -| `middleware` | `mi` | Generate a middleware declaration. | -| `module` | `mo` | Generate a module declaration. | -| `pipe` | `pi` | Generate a pipe declaration. | -| `provider` | `pr` | Generate a provider declaration. | -| `resolver` | `r` | Generate a resolver declaration. | -| `resource` | `res` | Generate a new CRUD resource. See the [CRUD (resource) generator](/recipes/crud-generator) for more details. (TS only)| -| `service` | `s` | Generate a service declaration. | +| Name | Alias | Description | +| ------------- | ----- | ---------------------------------------------------------------------------------------------------------------------- | +| `app` | | Generate a new application within a monorepo (converting to monorepo if it's a standard structure). | +| `library` | `lib` | Generate a new library within a monorepo (converting to monorepo if it's a standard structure). | +| `class` | `cl` | Generate a new class. | +| `controller` | `co` | Generate a controller declaration. | +| `decorator` | `d` | Generate a custom decorator. | +| `filter` | `f` | Generate a filter declaration. | +| `gateway` | `ga` | Generate a gateway declaration. | +| `guard` | `gu` | Generate a guard declaration. | +| `interface` | `itf` | Generate an interface. | +| `interceptor` | `itc` | Generate an interceptor declaration. | +| `middleware` | `mi` | Generate a middleware declaration. | +| `module` | `mo` | Generate a module declaration. | +| `pipe` | `pi` | Generate a pipe declaration. | +| `provider` | `pr` | Generate a provider declaration. | +| `resolver` | `r` | Generate a resolver declaration. | +| `resource` | `res` | Generate a new CRUD resource. See the [CRUD (resource) generator](/recipes/crud-generator) for more details. (TS only) | +| `service` | `s` | Generate a service declaration. | ##### Options @@ -107,15 +107,19 @@ $ nest build [options] ##### Options -| Option | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `--path [path]` | Path to `tsconfig` file.
Alias `-p` | -| `--config [path]` | Path to `nest-cli` configuration file.
Alias `-c` | -| `--watch` | Run in watch mode (live-reload).
If you're using `tsc` for compilation, you can type `rs` to restart the application (when `manualRestart` option is set to `true`).
Alias `-w` | -| `--builder [name]` | Specify the builder to use for compilation (`tsc`, `swc`, or `webpack`).
Alias `-b` | -| `--webpack` | Use webpack for compilation (deprecated: use `--builder webpack` instead). | -| `--webpackPath` | Path to webpack configuration. | -| `--tsc` | Force use `tsc` for compilation. | +| Option | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `--path [path]` | Path to `tsconfig` file.
Alias `-p` | +| `--config [path]` | Path to `nest-cli` configuration file.
Alias `-c` | +| `--watch` | Run in watch mode (live-reload).
If you're using `tsc` for compilation, you can type `rs` to restart the application (when `manualRestart` option is set to `true`).
Alias `-w` | +| `--builder [name]` | Specify the builder to use for compilation (`tsc`, `swc`, or `webpack`).
Alias `-b` | +| `--webpack` | Use webpack for compilation (deprecated: use `--builder webpack` instead). | +| `--webpackPath` | Path to webpack configuration. | +| `--tsc` | Force use `tsc` for compilation. | +| `--watchAssets` | Watch non-TS files (assets like `.graphql` etc.). See [Assets](cli/monorepo#assets) for more details. | +| `--type-check` | Enable type checking (when SWC is used). | +| `--all` | Build all projects in a monorepo. | +| `--preserveWatchOutput` | Keep outdated console output in watch mode instead of clearing the screen. (`tsc` watch mode only) | #### nest start @@ -133,20 +137,22 @@ $ nest start [options] ##### Options -| Option | Description | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `--path [path]` | Path to `tsconfig` file.
Alias `-p` | -| `--config [path]` | Path to `nest-cli` configuration file.
Alias `-c` | -| `--watch` | Run in watch mode (live-reload)
Alias `-w` | -| `--builder [name]` | Specify the builder to use for compilation (`tsc`, `swc`, or `webpack`).
Alias `-b` | -| `--preserveWatchOutput` | Keep outdated console output in watch mode instead of clearing the screen. (`tsc` watch mode only) | -| `--watchAssets` | Run in watch mode (live-reload), watching non-TS files (assets). See [Assets](cli/monorepo#assets) for more details. | -| `--debug [hostport]` | Run in debug mode (with --inspect flag)
Alias `-d` | -| `--webpack` | Use webpack for compilation. (deprecated: use `--builder webpack` instead) | -| `--webpackPath` | Path to webpack configuration. | -| `--tsc` | Force use `tsc` for compilation. | -| `--exec [binary]` | Binary to run (default: `node`).
Alias `-e` | -| `-- [key=value]` | Command-line arguments that can be referenced with `process.argv`. | +| Option | Description | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `--path [path]` | Path to `tsconfig` file.
Alias `-p` | +| `--config [path]` | Path to `nest-cli` configuration file.
Alias `-c` | +| `--watch` | Run in watch mode (live-reload)
Alias `-w` | +| `--builder [name]` | Specify the builder to use for compilation (`tsc`, `swc`, or `webpack`).
Alias `-b` | +| `--preserveWatchOutput` | Keep outdated console output in watch mode instead of clearing the screen. (`tsc` watch mode only) | +| `--watchAssets` | Run in watch mode (live-reload), watching non-TS files (assets). See [Assets](cli/monorepo#assets) for more details. | +| `--debug [hostport]` | Run in debug mode (with --inspect flag)
Alias `-d` | +| `--webpack` | Use webpack for compilation. (deprecated: use `--builder webpack` instead) | +| `--webpackPath` | Path to webpack configuration. | +| `--tsc` | Force use `tsc` for compilation. | +| `--exec [binary]` | Binary to run (default: `node`).
Alias `-e` | +| `--no-shell` | Do not spawn child processes within a shell (see node's `child_process.spawn()` method docs). | +| `--env-file` | Loads environment variables from a file relative to the current directory, making them available to applications on `process.env`. | +| `-- [key=value]` | Command-line arguments that can be referenced with `process.argv`. | #### nest add @@ -180,7 +186,7 @@ $ nest info [System Information] OS Version : macOS High Sierra -NodeJS Version : v16.18.0 +NodeJS Version : v20.18.0 [Nest Information] microservices version : 10.0.0 websockets version : 10.0.0 diff --git a/content/cli/workspaces.md b/content/cli/workspaces.md index e7afffa748..06ad6e9242 100644 --- a/content/cli/workspaces.md +++ b/content/cli/workspaces.md @@ -43,7 +43,7 @@ We've constructed a _standard mode_ structure, with a folder structure that look
nest-cli.json
package.json
tsconfig.json
-
.eslintrc.js
+
eslint.config.mjs
We can convert this to a monorepo mode structure as follows: @@ -84,7 +84,7 @@ At this point, `nest` converts the existing structure to a **monorepo mode** str
nest-cli.json
package.json
tsconfig.json
-
.eslintrc.js
+
eslint.config.mjs
The `generate app` schematic has reorganized the code - moving each **application** project under the `apps` folder, and adding a project-specific `tsconfig.app.json` file in each project's root folder. Our original `my-project` app has become the **default project** for the monorepo, and is now a peer with the just-added `my-app`, located under the `apps` folder. We'll cover default projects below. @@ -118,7 +118,7 @@ $ nest start my-app Application-type projects, or what we might informally refer to as just "applications", are complete Nest applications that you can run and deploy. You generate an application-type project with `nest generate app`. -This command automatically generates a project skeleton, including the standard `src` and `test` folders from the [typescript starter](https://github.com/nestjs/typescript-starter). Unlike standard mode, an application project in a monorepo does not have any of the package dependency (`package.json`) or other project configuration artifacts like `.prettierrc` and `.eslintrc.js`. Instead, the monorepo-wide dependencies and config files are used. +This command automatically generates a project skeleton, including the standard `src` and `test` folders from the [typescript starter](https://github.com/nestjs/typescript-starter). Unlike standard mode, an application project in a monorepo does not have any of the package dependency (`package.json`) or other project configuration artifacts like `.prettierrc` and `eslint.config.mjs`. Instead, the monorepo-wide dependencies and config files are used. However, the schematic does generate a project-specific `tsconfig.app.json` file in the root folder of the project. This config file automatically sets appropriate build options, including setting the compilation output folder properly. The file extends the top-level (monorepo) `tsconfig.json` file, so you can manage global settings monorepo-wide, but override them if needed at the project level. diff --git a/content/controllers.md b/content/controllers.md index b20cabca96..cc3fb32ba3 100644 --- a/content/controllers.md +++ b/content/controllers.md @@ -191,18 +191,18 @@ It's that simple. Nest provides decorators for all of the standard HTTP methods: #### Route wildcards -Pattern based routes are supported as well. For instance, the asterisk is used as a wildcard, and will match any combination of characters. +Pattern-based routes are also supported in NestJS. For example, the asterisk (`*`) can be used as a wildcard to match any combination of characters in a route at the end of a path. In the following example, the `findAll()` method will be executed for any route that starts with `abcd/`, regardless of the number of characters that follow. ```typescript -@Get('ab*cd') +@Get('abcd/*') findAll() { return 'This route uses a wildcard'; } ``` -The `'ab*cd'` route path will match `abcd`, `ab_cd`, `abecd`, and so on. The characters `?`, `+`, `*`, and `()` may be used in a route path, and are subsets of their regular expression counterparts. The hyphen ( `-`) and the dot (`.`) are interpreted literally by string-based paths. +The `'abcd/*'` route path will match `abcd/`, `abcd/123`, `abcd/abc`, and so on. The hyphen ( `-`) and the dot (`.`) are interpreted literally by string-based paths. -> warning **Warning** A wildcard in the middle of the route is only supported by express. +When it comes to asterisks used in the **middle of a route**, Express requires named wildcards (e.g., `ab{{ '{' }}*splat}cd`), while Fastify does not support them at all. #### Status code @@ -313,7 +313,7 @@ export class AdminController { } ``` -> **Warning** Since **Fastify** lacks support for nested routers, when using sub-domain routing, the (default) Express adapter should be used instead. +> warning **Warning** Since **Fastify** does not support nested routers, if you are using sub-domain routing, it is recommended to use the default Express adapter instead. Similar to a route `path`, the `hosts` option can use tokens to capture the dynamic value at that position in the host name. The host parameter token in the `@Controller()` decorator example below demonstrates this usage. Host parameters declared in this way can be accessed using the `@HostParam()` decorator, which should be added to the method signature. diff --git a/content/exception-filters.md b/content/exception-filters.md index e84c535e9f..d7dd7c12b6 100644 --- a/content/exception-filters.md +++ b/content/exception-filters.md @@ -90,6 +90,14 @@ Using the above, this is how the response would look: } ``` +#### Exceptions logging + +By default, the exception filter does not log built-in exceptions like `HttpException` (and any exceptions that inherit from it). When these exceptions are thrown, they won't appear in the console, as they are treated as part of the normal application flow. The same behavior applies to other built-in exceptions such as `WsException` and `RpcException`. + +These exceptions all inherit from the base `IntrinsicException` class, which is exported from the `@nestjs/common` package. This class helps differentiate between exceptions that are part of normal application operation and those that are not. + +If you want to log these exceptions, you can create a custom exception filter. We'll explain how to do this in the next section. + #### Custom exceptions In many cases, you will not need to write custom exceptions, and can use the built-in Nest HTTP exception, as described in the next section. If you do need to create customized exceptions, it's good practice to create your own **exceptions hierarchy**, where your custom exceptions inherit from the base `HttpException` class. With this approach, Nest will recognize your exceptions, and automatically take care of the error responses. Let's implement such a custom exception: diff --git a/content/first-steps.md b/content/first-steps.md index 4b3d93579e..f2dbc2c98a 100644 --- a/content/first-steps.md +++ b/content/first-steps.md @@ -10,7 +10,7 @@ We'll mostly use TypeScript in the examples we provide, but you can always **swi #### Prerequisites -Please make sure that [Node.js](https://nodejs.org) (version >= 16) is installed on your operating system. +Please make sure that [Node.js](https://nodejs.org) (version >= 20) is installed on your operating system. #### Setup diff --git a/content/microservices/basics.md b/content/microservices/basics.md index d21d4a64ec..18cd99526b 100644 --- a/content/microservices/basics.md +++ b/content/microservices/basics.md @@ -103,17 +103,19 @@ The second argument of the `createMicroservice()` method is an `options` object. -#### Patterns +> info **Hint** The above properties are specific to the TCP transporter. For information on available options for other transporters, refer to the relevant chapter. + +#### Message and Event Patterns Microservices recognize both messages and events by **patterns**. A pattern is a plain value, for example, a literal object or a string. Patterns are automatically serialized and sent over the network along with the data portion of a message. In this way, message senders and consumers can coordinate which requests are consumed by which handlers. #### Request-response -The request-response message style is useful when you need to **exchange** messages between various external services. With this paradigm, you can be certain that the service has actually received the message (without the need to manually implement a message ACK protocol). However, the request-response paradigm is not always the best choice. For example, streaming transporters that use log-based persistence, such as [Kafka](https://docs.confluent.io/3.0.0/streams/) or [NATS streaming](https://github.com/nats-io/node-nats-streaming), are optimized for solving a different range of issues, more aligned with an event messaging paradigm (see [event-based messaging](https://docs.nestjs.com/microservices/basics#event-based) below for more details). +The request-response message style is useful when you need to **exchange** messages between various external services. This paradigm ensures that the service has actually received the message (without requiring you to manually implement an acknowledgment protocol). However, the request-response approach may not always be the best fit. For example, streaming transporters, such as [Kafka](https://docs.confluent.io/3.0.0/streams/) or [NATS streaming](https://github.com/nats-io/node-nats-streaming), which use log-based persistence, are optimized for addressing a different set of challenges, more aligned with the event messaging paradigm (see [event-based messaging](https://docs.nestjs.com/microservices/basics#event-based) for more details). -To enable the request-response message type, Nest creates two logical channels - one is responsible for transferring the data while the other waits for incoming responses. For some underlying transports, such as [NATS](https://nats.io/), this dual-channel support is provided out-of-the-box. For others, Nest compensates by manually creating separate channels. There can be overhead for this, so if you do not require a request-response message style, you should consider using the event-based method. +To enable the request-response message type, Nest creates two logical channels: one for transferring data and another for waiting for incoming responses. For some underlying transports, like [NATS](https://nats.io/), this dual-channel support is provided out-of-the-box. For others, Nest compensates by manually creating separate channels. While this is effective, it can introduce some overhead. Therefore, if you don’t require a request-response message style, you may want to consider using the event-based method. -To create a message handler based on the request-response paradigm use the `@MessagePattern()` decorator, which is imported from the `@nestjs/microservices` package. This decorator should be used only within the [controller](https://docs.nestjs.com/controllers) classes since they are the entry points for your application. Using them inside providers won't have any effect as they are simply ignored by Nest runtime. +To create a message handler based on the request-response paradigm, use the `@MessagePattern()` decorator, which is imported from the `@nestjs/microservices` package. This decorator should only be used within [controller](https://docs.nestjs.com/controllers) classes, as they serve as the entry points for your application. Using it in providers will have no effect, as they will be ignored by the Nest runtime. ```typescript @@filename(math.controller) @@ -140,11 +142,11 @@ export class MathController { } ``` -In the above code, the `accumulate()` **message handler** listens for messages that fulfill the `{{ '{' }} cmd: 'sum' {{ '}' }}` message pattern. The message handler takes a single argument, the `data` passed from the client. In this case, the data is an array of numbers which are to be accumulated. +In the above code, the `accumulate()` **message handler** listens for messages that match the `{{ '{' }} cmd: 'sum' {{ '}' }}` message pattern. The message handler takes a single argument, the `data` passed from the client. In this case, the data is an array of numbers that need to be accumulated. #### Asynchronous responses -Message handlers are able to respond either synchronously or **asynchronously**. Hence, `async` methods are supported. +Message handlers can respond either synchronously or **asynchronously**, meaning that `async` methods are supported. ```typescript @@filename() @@ -159,7 +161,7 @@ async accumulate(data) { } ``` -A message handler is also able to return an `Observable`, in which case the result values will be emitted until the stream is completed. +A message handler can also return an `Observable`, in which case the result values will be emitted until the stream completes. ```typescript @@filename() @@ -174,15 +176,15 @@ accumulate(data: number[]): Observable { } ``` -In the example above, the message handler will respond **3 times** (with each item from the array). +In the example above, the message handler will respond **three times**, once for each item in the array. #### Event-based -While the request-response method is ideal for exchanging messages between services, it is less suitable when your message style is event-based - when you just want to publish **events** without waiting for a response. In that case, you do not want the overhead required by request-response for maintaining two channels. +While the request-response method is perfect for exchanging messages between services, it is less suited for event-based messaging—when you simply want to publish **events** without waiting for a response. In such cases, the overhead of maintaining two channels for request-response is unnecessary. -Suppose you would like to simply notify another service that a certain condition has occurred in this part of the system. This is the ideal use case for the event-based message style. +For example, if you want to notify another service that a specific condition has occurred in this part of the system, the event-based message style is ideal. -To create an event handler, we use the `@EventPattern()` decorator, which is imported from the `@nestjs/microservices` package. +To create an event handler, you can use the `@EventPattern()` decorator, which is imported from the `@nestjs/microservices` package. ```typescript @@filename() @@ -197,15 +199,15 @@ async handleUserCreated(data) { } ``` -> info **Hint** You can register multiple event handlers for a **single** event pattern and all of them will be automatically triggered in parallel. +> info **Hint** You can register multiple event handlers for a **single** event pattern, and all of them will be automatically triggered in parallel. The `handleUserCreated()` **event handler** listens for the `'user_created'` event. The event handler takes a single argument, the `data` passed from the client (in this case, an event payload which has been sent over the network). -#### Decorators +#### Additional request details -In more sophisticated scenarios, you may want to access more information about the incoming request. For example, in the case of NATS with wildcard subscriptions, you may want to get the original subject that the producer has sent the message to. Likewise, in Kafka you may want to access the message headers. In order to accomplish that, you can use built-in decorators as follows: +In more advanced scenarios, you might need to access additional details about the incoming request. For instance, when using NATS with wildcard subscriptions, you may want to retrieve the original subject that the producer sent the message to. Similarly, with Kafka, you may need to access the message headers. To achieve this, you can leverage built-in decorators as shown below: ```typescript @@filename() @@ -227,15 +229,15 @@ getDate(data, context) { > info **Hint** You can also pass in a property key to the `@Payload()` decorator to extract a specific property from the incoming payload object, for example, `@Payload('id')`. -#### Client +#### Client (producer class) -A client Nest application can exchange messages or publish events to a Nest microservice using the `ClientProxy` class. This class defines several methods, such as `send()` (for request-response messaging) and `emit()` (for event-driven messaging) that let you communicate with a remote microservice. Obtain an instance of this class in one of the following ways. +A client Nest application can exchange messages or publish events to a Nest microservice using the `ClientProxy` class. This class provides several methods, such as `send()` (for request-response messaging) and `emit()` (for event-driven messaging), enabling communication with a remote microservice. You can obtain an instance of this class in the following ways: -One technique is to import the `ClientsModule`, which exposes the static `register()` method. This method takes an argument which is an array of objects representing microservice transporters. Each such object has a `name` property, an optional `transport` property (default is `Transport.TCP`), and an optional transporter-specific `options` property. +One approach is to import the `ClientsModule`, which exposes the static `register()` method. This method takes an array of objects representing microservice transporters. Each object must include a `name` property, and optionally a `transport` property (defaulting to `Transport.TCP`), as well as an optional `options` property. -The `name` property serves as an **injection token** that can be used to inject an instance of a `ClientProxy` where needed. The value of the `name` property, as an injection token, can be an arbitrary string or JavaScript symbol, as described [here](https://docs.nestjs.com/fundamentals/custom-providers#non-class-based-provider-tokens). +The `name` property acts as an **injection token**, which you can use to inject an instance of `ClientProxy` wherever needed. The value of this `name` property can be any arbitrary string or JavaScript symbol, as described [here](https://docs.nestjs.com/fundamentals/custom-providers#non-class-based-provider-tokens). -The `options` property is an object that contains the same properties we saw in the `createMicroservice()` method earlier. +The `options` property is an object that includes the same properties we saw in the `createMicroservice()` method earlier. ```typescript @Module({ @@ -247,7 +249,7 @@ The `options` property is an object that contains the same properties we saw in }) ``` -Alternatively, you can use the `registerAsync()` method if you need to pass in configuration or perform any other asynchronous processes. +Alternatively, you can use the `registerAsync()` method if you need to provide configuration or perform any other asynchronous processes during the setup. ```typescript @Module({ @@ -279,7 +281,7 @@ constructor( > info **Hint** The `ClientsModule` and `ClientProxy` classes are imported from the `@nestjs/microservices` package. -At times we may need to fetch the transporter configuration from another service (say a `ConfigService`), rather than hard-coding it in our client application. To do this, we can register a [custom provider](/fundamentals/custom-providers) using the `ClientProxyFactory` class. This class has a static `create()` method, which accepts a transporter options object, and returns a customized `ClientProxy` instance. +At times, you may need to fetch the transporter configuration from another service (such as a `ConfigService`), rather than hard-coding it in your client application. To achieve this, you can register a [custom provider](/fundamentals/custom-providers) using the `ClientProxyFactory` class. This class provides a static `create()` method that accepts a transporter options object and returns a customized `ClientProxy` instance. ```typescript @Module({ @@ -357,17 +359,17 @@ async publish() { } ``` -The `emit()` method takes two arguments, `pattern` and `payload`. The `pattern`should match one defined in an `@EventPattern()` decorator. The `payload` is an event payload that we want to transmit to the remote microservice. This method returns a **hot `Observable`** (unlike the cold `Observable` returned by `send()`), which means that whether or not you explicitly subscribe to the observable, the proxy will immediately try to deliver the event. +The `emit()` method takes two arguments: `pattern` and `payload`. The `pattern` should match one defined in an `@EventPattern()` decorator, while the `payload` represents the event data that you want to transmit to the remote microservice. This method returns a **hot `Observable`** (in contrast to the cold `Observable` returned by `send()`), meaning that regardless of whether you explicitly subscribe to the observable, the proxy will immediately attempt to deliver the event. -#### Scopes +#### Request-scoping -For people coming from different programming language backgrounds, it might be unexpected to learn that in Nest, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. Remember that Node.js doesn't follow the request/response Multi-Threaded Stateless Model in which every request is processed by a separate thread. Hence, using singleton instances is fully **safe** for our applications. +For those coming from different programming language backgrounds, it may be surprising to learn that in Nest, most things are shared across incoming requests. This includes a connection pool to the database, singleton services with global state, and more. Keep in mind that Node.js does not follow the request/response multi-threaded stateless model, where each request is processed by a separate thread. As a result, using singleton instances is **safe** for our applications. -However, there are edge-cases when request-based lifetime of the handler may be the desired behavior, for instance per-request caching in GraphQL applications, request tracking or multi-tenancy. Learn how to control scopes [here](/fundamentals/injection-scopes). +However, there are edge cases where a request-based lifetime for the handler might be desirable. This could include scenarios like per-request caching in GraphQL applications, request tracking, or multi-tenancy. You can learn more about how to control scopes [here](/fundamentals/injection-scopes). -Request-scoped handlers and providers can inject `RequestContext` using the `@Inject()` decorator in combination with `CONTEXT` token: +Request-scoped handlers and providers can inject `RequestContext` using the `@Inject()` decorator in combination with the `CONTEXT` token: ```typescript import { Injectable, Scope, Inject } from '@nestjs/common'; @@ -390,21 +392,80 @@ export interface RequestContext { The `data` property is the message payload sent by the message producer. The `pattern` property is the pattern used to identify an appropriate handler to handle the incoming message. +#### Instance status updates + +To get real-time updates on the connection and the state of the underlying driver instance, you can subscribe to the `status` stream. This stream provides status updates specific to the chosen driver. For instance, if you’re using the TCP transporter (the default), the `status` stream emits `connected` and `disconnected` events. + +```typescript +this.client.status.subscribe((status: TcpStatus) => { + console.log(status); +}); +``` + +> info **Hint** The `TcpStatus` type is imported from the `@nestjs/microservices` package. + +Similarly, you can subscribe to the server's `status` stream to receive notifications about the server's status. + +```typescript +const server = app.connectMicroservice(...); +server.status.subscribe((status: TcpStatus) => { + console.log(status); +}); +``` + +#### Listening to internal events + +In some cases, you might want to listen to internal events emitted by the microservice. For example, you could listen for the `error` event to trigger additional operations when an error occurs. To do this, use the `on()` method, as shown below: + +```typescript +this.client.on('error', (err) => { + console.error(err); +}); +``` + +Similarly, you can listen to the server's internal events: + +```typescript +server.on('error', (err) => { + console.error(err); +}); +``` + +> info **Hint** The `TcpEvents` type is imported from the `@nestjs/microservices` package. + +#### Underlying driver access + +For more advanced use cases, you may need to access the underlying driver instance. This can be useful for scenarios like manually closing the connection or using driver-specific methods. However, keep in mind that for most cases, you **shouldn't need** to access the driver directly. + +To do so, you can use the `unwrap()` method, which returns the underlying driver instance. The generic type parameter should specify the type of driver instance you expect. + +```typescript +const netServer = this.client.unwrap(); +``` + +Here, `Server` is a type imported from the `net` module. + +Similarly, you can access the server's underlying driver instance: + +```typescript +const netServer = server.unwrap(); +``` + #### Handling timeouts -In distributed systems, sometimes microservices might be down or not available. To avoid infinitely long waiting, you can use Timeouts. A timeout is an incredibly useful pattern when communicating with other services. To apply timeouts to your microservice calls, you can use the [RxJS](https://rxjs.dev) `timeout` operator. If the microservice does not respond to the request within a certain time, an exception is thrown, which can be caught and handled appropriately. +In distributed systems, microservices might sometimes be down or unavailable. To prevent indefinitely long waiting, you can use timeouts. A timeout is a highly useful pattern when communicating with other services. To apply timeouts to your microservice calls, you can use the [RxJS](https://rxjs.dev) `timeout` operator. If the microservice does not respond within the specified time, an exception is thrown, which you can catch and handle appropriately. -To solve this problem you have to use [`rxjs`](https://github.com/ReactiveX/rxjs) package. Just use the `timeout` operator in the pipe: +To implement this, you'll need to use the [`rxjs`](https://github.com/ReactiveX/rxjs) package. Simply use the `timeout` operator within the pipe: ```typescript @@filename() this.client - .send(pattern, data) - .pipe(timeout(5000)); + .send(pattern, data) + .pipe(timeout(5000)); @@switch this.client - .send(pattern, data) - .pipe(timeout(5000)); + .send(pattern, data) + .pipe(timeout(5000)); ``` > info **Hint** The `timeout` operator is imported from the `rxjs/operators` package. diff --git a/content/microservices/kafka.md b/content/microservices/kafka.md index fbeb3dd57a..0fa55d704f 100644 --- a/content/microservices/kafka.md +++ b/content/microservices/kafka.md @@ -120,9 +120,9 @@ The `options` property is specific to the chosen transporter. The Kafka< #### Client -There is a small difference in Kafka compared to other microservice transporters. Instead of the `ClientProxy` class, we use the `ClientKafka` class. +There is a small difference in Kafka compared to other microservice transporters. Instead of the `ClientProxy` class, we use the `ClientKafkaProxy` class. -Like other microservice transporters, you have several options for creating a `ClientKafka` instance. +Like other microservice transporters, you have several options for creating a `ClientKafkaProxy` instance. One method for creating an instance is to use the `ClientsModule`. To create a client instance with the `ClientsModule`, import it and use the `register()` method to pass an options object with the same properties shown above in the `createMicroservice()` method, as well as a `name` property to be used as the injection token. Read more about `ClientsModule` here. @@ -166,26 +166,26 @@ Use the `@Client()` decorator as follows: } } }) -client: ClientKafka; +client: ClientKafkaProxy; ``` #### Message pattern -The Kafka microservice message pattern utilizes two topics for the request and reply channels. The `ClientKafka#send()` method sends messages with a [return address](https://www.enterpriseintegrationpatterns.com/patterns/messaging/ReturnAddress.html) by associating a [correlation id](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CorrelationIdentifier.html), reply topic, and reply partition with the request message. This requires the `ClientKafka` instance to be subscribed to the reply topic and assigned to at least one partition before sending a message. +The Kafka microservice message pattern utilizes two topics for the request and reply channels. The `ClientKafkaProxy#send()` method sends messages with a [return address](https://www.enterpriseintegrationpatterns.com/patterns/messaging/ReturnAddress.html) by associating a [correlation id](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CorrelationIdentifier.html), reply topic, and reply partition with the request message. This requires the `ClientKafkaProxy` instance to be subscribed to the reply topic and assigned to at least one partition before sending a message. Subsequently, you need to have at least one reply topic partition for every Nest application running. For example, if you are running 4 Nest applications but the reply topic only has 3 partitions, then 1 of the Nest applications will error out when trying to send a message. -When new `ClientKafka` instances are launched they join the consumer group and subscribe to their respective topics. This process triggers a rebalance of topic partitions assigned to consumers of the consumer group. +When new `ClientKafkaProxy` instances are launched they join the consumer group and subscribe to their respective topics. This process triggers a rebalance of topic partitions assigned to consumers of the consumer group. Normally, topic partitions are assigned using the round robin partitioner, which assigns topic partitions to a collection of consumers sorted by consumer names which are randomly set on application launch. However, when a new consumer joins the consumer group, the new consumer can be positioned anywhere within the collection of consumers. This creates a condition where pre-existing consumers can be assigned different partitions when the pre-existing consumer is positioned after the new consumer. As a result, the consumers that are assigned different partitions will lose response messages of requests sent before the rebalance. -To prevent the `ClientKafka` consumers from losing response messages, a Nest-specific built-in custom partitioner is utilized. This custom partitioner assigns partitions to a collection of consumers sorted by high-resolution timestamps (`process.hrtime()`) that are set on application launch. +To prevent the `ClientKafkaProxy` consumers from losing response messages, a Nest-specific built-in custom partitioner is utilized. This custom partitioner assigns partitions to a collection of consumers sorted by high-resolution timestamps (`process.hrtime()`) that are set on application launch. #### Message response subscription -> warning **Note** This section is only relevant if you use [request-response](/microservices/basics#request-response) message style (with the `@MessagePattern` decorator and the `ClientKafka#send` method). Subscribing to the response topic is not necessary for the [event-based](/microservices/basics#event-based) communication (`@EventPattern` decorator and `ClientKafka#emit` method). +> warning **Note** This section is only relevant if you use [request-response](/microservices/basics#request-response) message style (with the `@MessagePattern` decorator and the `ClientKafkaProxy#send` method). Subscribing to the response topic is not necessary for the [event-based](/microservices/basics#event-based) communication (`@EventPattern` decorator and `ClientKafkaProxy#emit` method). -The `ClientKafka` class provides the `subscribeToResponseOf()` method. The `subscribeToResponseOf()` method takes a request's topic name as an argument and adds the derived reply topic name to a collection of reply topics. This method is required when implementing the message pattern. +The `ClientKafkaProxy` class provides the `subscribeToResponseOf()` method. The `subscribeToResponseOf()` method takes a request's topic name as an argument and adds the derived reply topic name to a collection of reply topics. This method is required when implementing the message pattern. ```typescript @@filename(heroes.controller) @@ -194,7 +194,7 @@ onModuleInit() { } ``` -If the `ClientKafka` instance is created asynchronously, the `subscribeToResponseOf()` method must be called before calling the `connect()` method. +If the `ClientKafkaProxy` instance is created asynchronously, the `subscribeToResponseOf()` method must be called before calling the `connect()` method. ```typescript @@filename(heroes.controller) @@ -210,7 +210,7 @@ Nest receives incoming Kafka messages as an object with `key`, `value`, and `hea #### Outgoing -Nest sends outgoing Kafka messages after a serialization process when publishing events or sending messages. This occurs on arguments passed to the `ClientKafka` `emit()` and `send()` methods or on values returned from a `@MessagePattern` method. This serialization "stringifies" objects that are not strings or buffers by using `JSON.stringify()` or the `toString()` prototype method. +Nest sends outgoing Kafka messages after a serialization process when publishing events or sending messages. This occurs on arguments passed to the `ClientKafkaProxy` `emit()` and `send()` methods or on values returned from a `@MessagePattern` method. This serialization "stringifies" objects that are not strings or buffers by using `JSON.stringify()` or the `toString()` prototype method. ```typescript @@filename(heroes.controller) @@ -294,7 +294,7 @@ Check out these two sections to learn more about this: [Overview: Event-based](/ #### Context -In more sophisticated scenarios, you may want to access more information about the incoming request. When using the Kafka transporter, you can access the `KafkaContext` object. +In more complex scenarios, you may need to access additional information about the incoming request. When using the Kafka transporter, you can access the `KafkaContext` object. ```typescript @@filename() @@ -369,7 +369,7 @@ async killDragon(@Payload() message: KillDragonMessage, @Ctx() context: KafkaCon #### Naming conventions -The Kafka microservice components append a description of their respective role onto the `client.clientId` and `consumer.groupId` options to prevent collisions between Nest microservice client and server components. By default the `ClientKafka` components append `-client` and the `ServerKafka` components append `-server` to both of these options. Note how the provided values below are transformed in that way (as shown in the comments). +The Kafka microservice components append a description of their respective role onto the `client.clientId` and `consumer.groupId` options to prevent collisions between Nest microservice client and server components. By default the `ClientKafkaProxy` components append `-client` and the `ServerKafka` components append `-server` to both of these options. Note how the provided values below are transformed in that way (as shown in the comments). ```typescript @@filename(main) @@ -403,10 +403,10 @@ And for the client: } } }) -client: ClientKafka; +client: ClientKafkaProxy; ``` -> info **Hint** Kafka client and consumer naming conventions can be customized by extending `ClientKafka` and `KafkaServer` in your own custom provider and overriding the constructor. +> info **Hint** Kafka client and consumer naming conventions can be customized by extending `ClientKafkaProxy` and `KafkaServer` in your own custom provider and overriding the constructor. Since the Kafka microservice message pattern utilizes two topics for the request and reply channels, a reply pattern should be derived from the request topic. By default, the name of the reply topic is the composite of the request topic name with `.reply` appended. @@ -417,7 +417,7 @@ onModuleInit() { } ``` -> info **Hint** Kafka reply topic naming conventions can be customized by extending `ClientKafka` in your own custom provider and overriding the `getResponsePatternName` method. +> info **Hint** Kafka reply topic naming conventions can be customized by extending `ClientKafkaProxy` in your own custom provider and overriding the `getResponsePatternName` method. #### Retriable exceptions @@ -442,7 +442,7 @@ Committing offsets is essential when working with Kafka. Per default, messages w @EventPattern('user.created') async handleUserCreated(@Payload() data: IncomingMessage, @Ctx() context: KafkaContext) { // business logic - + const { offset } = context.getMessage(); const partition = context.getPartition(); const topic = context.getTopic(); @@ -491,3 +491,35 @@ const app = await NestFactory.createMicroservice(AppModule, { } }); ``` + +#### Instance status updates + +To get real-time updates on the connection and the state of the underlying driver instance, you can subscribe to the `status` stream. This stream provides status updates specific to the chosen driver. For the Kafka driver, the `status` stream emits `connected`, `disconnected`, `rebalancing`, `crashed`, and `stopped` events. + +```typescript +this.client.status.subscribe((status: KafkaStatus) => { + console.log(status); +}); +``` + +> info **Hint** The `KafkaStatus` type is imported from the `@nestjs/microservices` package. + +Similarly, you can subscribe to the server's `status` stream to receive notifications about the server's status. + +```typescript +const server = app.connectMicroservice(...); +server.status.subscribe((status: KafkaStatus) => { + console.log(status); +}); +``` + +#### Underlying producer and consumer + +For more advanced use cases, you may need to access the underlying prodocuer and consumer instances. This can be useful for scenarios like manually closing the connection or using driver-specific methods. However, keep in mind that for most cases, you **shouldn't need** to access the driver directly. + +To do so, you can use `producer` and `consumer` getters exposed by the `ClientKafkaProxy` instance. + +```typescript +const producer = this.client.producer; +const consumer = this.client.consumer; +``` diff --git a/content/microservices/mqtt.md b/content/microservices/mqtt.md index 236e1ccd1f..9a4519e14a 100644 --- a/content/microservices/mqtt.md +++ b/content/microservices/mqtt.md @@ -64,7 +64,7 @@ Other options to create a client (either `ClientProxyFactory` or `@Client()`) ca #### Context -In more sophisticated scenarios, you may want to access more information about the incoming request. When using the MQTT transporter, you can access the `MqttContext` object. +In more complex scenarios, you may need to access additional information about the incoming request. When using the MQTT transporter, you can access the `MqttContext` object. ```typescript @@filename() @@ -119,6 +119,7 @@ getTemperature(context) { #### Quality of Service (QoS) Any subscription created with `@MessagePattern` or `@EventPattern` decorators will subscribe with QoS 0. If a higher QoS is required, it can be set globally using the `subscribeOptions` block when establishing the connection as follows: + ```typescript @@filename(main) const app = await NestFactory.createMicroservice(AppModule, { @@ -141,6 +142,7 @@ const app = await NestFactory.createMicroservice(AppModule, { }, }); ``` + If a topic specific QoS is required, consider creating a [Custom transporter](https://docs.nestjs.com/microservices/custom-transport). #### Record builders @@ -199,3 +201,60 @@ import { ClientProxyFactory, Transport } from '@nestjs/microservices'; }) export class ApiModule {} ``` + +#### Instance status updates + +To get real-time updates on the connection and the state of the underlying driver instance, you can subscribe to the `status` stream. This stream provides status updates specific to the chosen driver. For the MQTT driver, the `status` stream emits `connected`, `disconnected`, `reconnecting`, and `closed` events. + +```typescript +this.client.status.subscribe((status: MqttStatus) => { + console.log(status); +}); +``` + +> info **Hint** The `MqttStatus` type is imported from the `@nestjs/microservices` package. + +Similarly, you can subscribe to the server's `status` stream to receive notifications about the server's status. + +```typescript +const server = app.connectMicroservice(...); +server.status.subscribe((status: MqttStatus) => { + console.log(status); +}); +``` + +#### Listening to MQTT events + +In some cases, you might want to listen to internal events emitted by the microservice. For example, you could listen for the `error` event to trigger additional operations when an error occurs. To do this, use the `on()` method, as shown below: + +```typescript +this.client.on('error', (err) => { + console.error(err); +}); +``` + +Similarly, you can listen to the server's internal events: + +```typescript +server.on('error', (err) => { + console.error(err); +}); +``` + +> info **Hint** The `MqttEvents` type is imported from the `@nestjs/microservices` package. + +#### Underlying driver access + +For more advanced use cases, you may need to access the underlying driver instance. This can be useful for scenarios like manually closing the connection or using driver-specific methods. However, keep in mind that for most cases, you **shouldn't need** to access the driver directly. + +To do so, you can use the `unwrap()` method, which returns the underlying driver instance. The generic type parameter should specify the type of driver instance you expect. + +```typescript +const mqttClient = this.client.unwrap(); +``` + +Similarly, you can access the server's underlying driver instance: + +```typescript +const mqttClient = server.unwrap(); +``` diff --git a/content/microservices/nats.md b/content/microservices/nats.md index d8a6805a17..af0f3f7b63 100644 --- a/content/microservices/nats.md +++ b/content/microservices/nats.md @@ -35,8 +35,23 @@ const app = await NestFactory.createMicroservice(AppModule, { #### Options -The `options` object is specific to the chosen transporter. The NATS transporter exposes the properties described [here](https://github.com/nats-io/node-nats#connection-options). -Additionally, there is a `queue` property which allows you to specify the name of the queue that your server should subscribe to (leave `undefined` to ignore this setting). Read more about NATS queue groups below. +The `options` object is specific to the chosen transporter. The NATS transporter exposes the properties described [here](https://github.com/nats-io/node-nats#connection-options) as well as the following properties: + + + + + + + + + + + + +
queueQueue that your server should subscribe to (leave undefined to ignore this setting). Read more about NATS queue groups below. +
gracefulShutdownEnables graceful shutdown. When enabled, the server first unsubscribes from all channels before closing the connection. Default is false. +
gracePeriodTime in milliseconds to wait for the server after unsubscribing from all channels. Default is 10000 ms. +
#### Client @@ -88,7 +103,7 @@ const app = await NestFactory.createMicroservice(AppModule, #### Context -In more sophisticated scenarios, you may want to access more information about the incoming request. When using the NATS transporter, you can access the `NatsContext` object. +In more complex scenarios, you may need to access additional information about the incoming request. When using the NATS transporter, you can access the `NatsContext` object. ```typescript @@filename() @@ -184,3 +199,60 @@ import { ClientProxyFactory, Transport } from '@nestjs/microservices'; }) export class ApiModule {} ``` + +#### Instance status updates + +To get real-time updates on the connection and the state of the underlying driver instance, you can subscribe to the `status` stream. This stream provides status updates specific to the chosen driver. For the NATS driver, the `status` stream emits `connected`, `disconnected`, and `reconnecting` events. + +```typescript +this.client.status.subscribe((status: NatsStatus) => { + console.log(status); +}); +``` + +> info **Hint** The `NatsStatus` type is imported from the `@nestjs/microservices` package. + +Similarly, you can subscribe to the server's `status` stream to receive notifications about the server's status. + +```typescript +const server = app.connectMicroservice(...); +server.status.subscribe((status: NatsStatus) => { + console.log(status); +}); +``` + +#### Listening to Nats events + +In some cases, you might want to listen to internal events emitted by the microservice. For example, you could listen for the `error` event to trigger additional operations when an error occurs. To do this, use the `on()` method, as shown below: + +```typescript +this.client.on('error', (err) => { + console.error(err); +}); +``` + +Similarly, you can listen to the server's internal events: + +```typescript +server.on('error', (err) => { + console.error(err); +}); +``` + +> info **Hint** The `NatsEvents` type is imported from the `@nestjs/microservices` package. + +#### Underlying driver access + +For more advanced use cases, you may need to access the underlying driver instance. This can be useful for scenarios like manually closing the connection or using driver-specific methods. However, keep in mind that for most cases, you **shouldn't need** to access the driver directly. + +To do so, you can use the `unwrap()` method, which returns the underlying driver instance. The generic type parameter should specify the type of driver instance you expect. + +```typescript +const natsConnection = this.client.unwrap(); +``` + +Similarly, you can access the server's underlying driver instance: + +```typescript +const natsConnection = server.unwrap(); +``` diff --git a/content/microservices/rabbitmq.md b/content/microservices/rabbitmq.md index 607945abe5..e88b722c36 100644 --- a/content/microservices/rabbitmq.md +++ b/content/microservices/rabbitmq.md @@ -115,7 +115,7 @@ Other options to create a client (either `ClientProxyFactory` or `@Client()`) ca #### Context -In more sophisticated scenarios, you may want to access more information about the incoming request. When using the RabbitMQ transporter, you can access the `RmqContext` object. +In more complex scenarios, you may need to access additional information about the incoming request. When using the RabbitMQ transporter, you can access the `RmqContext` object. ```typescript @@filename() @@ -241,3 +241,62 @@ replaceEmoji(data, context) { return headers['x-version'] === '1.0.0' ? '🐱' : '🐈'; } ``` + +#### Instance status updates + +To get real-time updates on the connection and the state of the underlying driver instance, you can subscribe to the `status` stream. This stream provides status updates specific to the chosen driver. For the RMQ driver, the `status` stream emits `connected` and `disconnected` events. + +```typescript +this.client.status.subscribe((status: RmqStatus) => { + console.log(status); +}); +``` + +> info **Hint** The `RmqStatus` type is imported from the `@nestjs/microservices` package. + +Similarly, you can subscribe to the server's `status` stream to receive notifications about the server's status. + +```typescript +const server = app.connectMicroservice(...); +server.status.subscribe((status: RmqStatus) => { + console.log(status); +}); +``` + +#### Listening to RabbitMQ events + +In some cases, you might want to listen to internal events emitted by the microservice. For example, you could listen for the `error` event to trigger additional operations when an error occurs. To do this, use the `on()` method, as shown below: + +```typescript +this.client.on('error', (err) => { + console.error(err); +}); +``` + +Similarly, you can listen to the server's internal events: + +```typescript +server.on('error', (err) => { + console.error(err); +}); +``` + +> info **Hint** The `RmqEvents` type is imported from the `@nestjs/microservices` package. + +#### Underlying driver access + +For more advanced use cases, you may need to access the underlying driver instance. This can be useful for scenarios like manually closing the connection or using driver-specific methods. However, keep in mind that for most cases, you **shouldn't need** to access the driver directly. + +To do so, you can use the `unwrap()` method, which returns the underlying driver instance. The generic type parameter should specify the type of driver instance you expect. + +```typescript +const managerRef = + this.client.unwrap(); +``` + +Similarly, you can access the server's underlying driver instance: + +```typescript +const managerRef = + server.unwrap(); +``` diff --git a/content/microservices/redis.md b/content/microservices/redis.md index 3010b0a4ac..f1a9a79b46 100644 --- a/content/microservices/redis.md +++ b/content/microservices/redis.md @@ -94,7 +94,7 @@ Other options to create a client (either `ClientProxyFactory` or `@Client()`) ca #### Context -In more sophisticated scenarios, you may want to access more information about the incoming request. When using the Redis transporter, you can access the `RedisContext` object. +In more complex scenarios, you may need to access additional information about the incoming request. When using the Redis transporter, you can access the `RedisContext` object. ```typescript @@filename() @@ -111,3 +111,86 @@ getNotifications(data, context) { ``` > info **Hint** `@Payload()`, `@Ctx()` and `RedisContext` are imported from the `@nestjs/microservices` package. + +#### Wildcards + +To enable wildcards support, set the `wildcards` option to `true`. This instructs the transporter to use `psubscribe` and `pmessage` under the hood. + +```typescript +const app = await NestFactory.createMicroservice(AppModule, { + transport: Transport.REDIS, + options: { + // Other options + wildcards: true, + }, +}); +``` + +Make sure to pass the `wildcards` option when creating a client instance as well. + +With this option enabled, you can use wildcards in your message and event patterns. For example, to subscribe to all channels starting with `notifications`, you can use the following pattern: + +```typescript +@EventPattern('notifications.*') +``` + +#### Instance status updates + +To get real-time updates on the connection and the state of the underlying driver instance, you can subscribe to the `status` stream. This stream provides status updates specific to the chosen driver. For the Redis driver, the `status` stream emits `connected`, `disconnected`, and `reconnecting` events. + +```typescript +this.client.status.subscribe((status: RedisStatus) => { + console.log(status); +}); +``` + +> info **Hint** The `RedisStatus` type is imported from the `@nestjs/microservices` package. + +Similarly, you can subscribe to the server's `status` stream to receive notifications about the server's status. + +```typescript +const server = app.connectMicroservice(...); +server.status.subscribe((status: RedisStatus) => { + console.log(status); +}); +``` + +#### Listening to Redis events + +In some cases, you might want to listen to internal events emitted by the microservice. For example, you could listen for the `error` event to trigger additional operations when an error occurs. To do this, use the `on()` method, as shown below: + +```typescript +this.client.on('error', (err) => { + console.error(err); +}); +``` + +Similarly, you can listen to the server's internal events: + +```typescript +server.on('error', (err) => { + console.error(err); +}); +``` + +> info **Hint** The `RedisEvents` type is imported from the `@nestjs/microservices` package. + +#### Underlying driver access + +For more advanced use cases, you may need to access the underlying driver instance. This can be useful for scenarios like manually closing the connection or using driver-specific methods. However, keep in mind that for most cases, you **shouldn't need** to access the driver directly. + +To do so, you can use the `unwrap()` method, which returns the underlying driver instance. The generic type parameter should specify the type of driver instance you expect. + +```typescript +const [pub, sub] = + this.client.unwrap<[import('ioredis').Redis, import('ioredis').Redis]>(); +``` + +Similarly, you can access the server's underlying driver instance: + +```typescript +const [pub, sub] = + server.unwrap<[import('ioredis').Redis, import('ioredis').Redis]>(); +``` + +Note that, in contrary to other transporters, the Redis transporter returns a tuple of two `ioredis` instances: the first one is used for publishing messages, and the second one is used for subscribing to messages. diff --git a/content/middlewares.md b/content/middlewares.md index 05bfe04d37..f75592d2e1 100644 --- a/content/middlewares.md +++ b/content/middlewares.md @@ -128,18 +128,23 @@ export class AppModule { #### Route wildcards -Pattern based routes are supported as well. For instance, the asterisk is used as a **wildcard**, and will match any combination of characters: +Pattern-based routes are also supported in NestJS middleware. For example, the named wildcard (`*splat`) can be used as a wildcard to match any combination of characters in a route. In the following example, the middleware will be executed for any route that starts with `abcd/`, regardless of the number of characters that follow. ```typescript forRoutes({ - path: 'ab*cd', + path: 'abcd/*splat', method: RequestMethod.ALL, }); ``` -The `'ab*cd'` route path will match `abcd`, `ab_cd`, `abecd`, and so on. The characters `?`, `+`, `*`, and `()` may be used in a route path, and are subsets of their regular expression counterparts. The hyphen ( `-`) and the dot (`.`) are interpreted literally by string-based paths. +The `'abcd/*'` route path will match `abcd/1`, `abcd/123`, `abcd/abc`, and so on. The hyphen ( `-`) and the dot (`.`) are interpreted literally by string-based paths. However, `abcd/` with no additional characters will not match the route. For this, you need to wrap the wildcard in braces to make it optional: -> warning **Warning** The `fastify` package uses the latest version of the `path-to-regexp` package, which no longer supports wildcard asterisks `*`. Instead, you must use parameters (e.g., `(.*)`, `:splat*`). +```typescript +forRoutes({ + path: 'abcd/{*splat}', + method: RequestMethod.ALL, +}); +``` #### Middleware consumer @@ -184,7 +189,9 @@ export class AppModule { #### Excluding routes -At times we want to **exclude** certain routes from having the middleware applied. We can easily exclude certain routes with the `exclude()` method. This method can take a single string, multiple strings, or a `RouteInfo` object identifying routes to be excluded, as shown below: +At times, we may want to **exclude** certain routes from having middleware applied. This can be easily achieved using the `exclude()` method. The `exclude()` method accepts a single string, multiple strings, or a `RouteInfo` object to identify the routes to be excluded. + +Here's an example of how to use it: ```typescript consumer @@ -192,7 +199,7 @@ consumer .exclude( { path: 'cats', method: RequestMethod.GET }, { path: 'cats', method: RequestMethod.POST }, - 'cats/(.*)', + 'cats/{*splat}', ) .forRoutes(CatsController); ``` @@ -201,6 +208,8 @@ consumer With the example above, `LoggerMiddleware` will be bound to all routes defined inside `CatsController` **except** the three passed to the `exclude()` method. +This approach provides flexibility in applying or excluding middleware based on specific routes or route patterns. + #### Functional middleware The `LoggerMiddleware` class we've been using is quite simple. It has no members, no additional methods, and no dependencies. Why can't we just define it in a simple function instead of a class? In fact, we can. This type of middleware is called **functional middleware**. Let's transform the logger middleware from class-based into functional middleware to illustrate the difference: diff --git a/content/migration.md b/content/migration.md index 209f2c3b33..9a023c0abc 100644 --- a/content/migration.md +++ b/content/migration.md @@ -1,33 +1,167 @@ ### Migration guide -This article provides a set of guidelines for migrating from Nest version 9 to version 10. -To learn more about the new features we've added in v10, check out this [article](https://trilon.io/blog/nestjs-10-is-now-available). -There were some very minor breaking changes that shouldn't affect most users - you can find the full list of them [here](https://github.com/nestjs/nest/releases/tag/v10.0.0). +This article offers a comprehensive guide for migrating from NestJS version 10 to version 11. To explore the new features introduced in v11, take a look at [this article](#). While the update includes a few minor breaking changes, they are unlikely to impact most users. You can review the complete list of breaking changes [here](#). #### Upgrading packages -While you can upgrade your packages manually, we recommend using [ncu (npm check updates)](https://npmjs.com/package/npm-check-updates). +Although you can manually upgrade your packages, we recommend using [npm-check-updates (ncu)](https://npmjs.com/package/npm-check-updates) for a more streamlined process. + +#### Express v5 + +After years of development, Express v5 was officially released in 2024 and became a stable version in 2025. With NestJS 11, Express v5 is now the default version integrated into the framework. While this update is seamless for most users, it’s important to be aware that Express v5 introduces some breaking changes. For detailed guidance, refer to the [Express v5 migration guide](https://expressjs.com/en/guide/migrating-5.html). + +One of the most notable updates in Express v5 is the revised path route matching algorithm. The following changes have been introduced to how path strings are matched with incoming requests: + +- The wildcard `*` must have a name, matching the behavior of parameters :, use `/*splat` or `/{{ '{' }}*splat}` instead of `/*` +- The optional character `?` is no longer supported, use braces instead: `/:file{{ '{' }}.:ext}`. +- Regexp characters are not supported. +- Some characters have been reserved to avoid confusion during upgrade `(()[]?+!)`, use `\` to escape them. +- Parameter names now support valid JavaScript identifiers, or quoted like `:"this"`. + +That said, routes that previously worked in Express v4 may not work in Express v5. For example: + +```typescript +@Get('users/*') +findAll() { + // In NestJS 11, this will be automatically converted to a valid Express v5 route. + // While it may still work, it's no longer advisable to use this wildcard syntax in Express v5. + return 'This route should not work in Express v5'; +} +``` + +To fix this issue, you can update the route to use a named wildcard: + +```typescript +@Get('users/*splat') +findAll() { + return 'This route will work in Express v5'; +} +``` + +> warning **Warning** Note that `*splat` is a named wildcard that matches any path without the root path. If you need to match the root path as well (`/users`), you can use `/users/{{ '{' }}*splat}`, wrapping the wildcard in braces (optional group). + +Similarly, if you have a middleware that runs on all routes, you may need to update the path to use a named wildcard: + +```typescript +// In NestJS 11, this will be automatically converted to a valid Express v5 route. +// While it may still work, it's no longer advisable to use this wildcard syntax in Express v5. +forRoutes('*'); // <-- This should not work in Express v5 +``` + +Instead, you can update the path to use a named wildcard: + +```typescript +forRoutes('{*splat}'); // <-- This will work in Express v5 +``` + +Note that `{{ '{' }}*splat}` is a named wildcard that matches any path including the root path. Outer braces make path optional. + +#### Fastify v5 + +Fastify v5 was released in 2024 and is now the default version integrated into NestJS 11. This update should be seamless for most users; however, Fastify v5 introduces a few breaking changes, though these are unlikely to affect the majority of NestJS users. For more detailed information, refer to the [Fastify v5 migration guide](https://fastify.dev/docs/v5.1.x/Guides/Migration-Guide-V5/). + +> info **Hint** There have been no changes to path matching in Fastify v5, so you can continue using the wildcard syntax as you did before. The behavior remains the same, and routes defined with wildcards (like `*`) will still work as expected. + +#### Module resolution algorithm + +Starting with NestJS 11, the module resolution algorithm has been improved to enhance performance and reduce memory usage for most applications. This change does not require any manual intervention, but there are some edge cases where the behavior may differ from previous versions. + +In NestJS v10 and earlier, dynamic modules were assigned a unique opaque key generated from the module's dynamic metadata. This key was used to identify the module in the module registry. For example, if you included `TypeOrmModule.forFeature([User])` in multiple modules, NestJS would deduplicate the modules and treat them as a single module node in the registry. This process is known as node deduplication. + +With the release of NestJS v11, we no longer generate predictable hashes for dynamic modules. Instead, object references are now used to determine if one module is equivalent to another. To share the same dynamic module across multiple modules, simply assign it to a variable and import it wherever needed. This new approach provides more flexibility and ensures that dynamic modules are handled more efficiently. + +#### Reflector type inference + +NestJS 11 introduces several improvements to the `Reflector` class, enhancing its functionality and type inference for metadata values. These updates provide a more intuitive and robust experience when working with metadata. + +1. `getAllAndMerge` now returns an object rather than an array containing a single element when there is only one metadata entry, and the `value` is of type `object`. This change improves consistency when dealing with object-based metadata. +2. The `getAllAndOverride` return type has been updated to `T | undefined` instead of `T`. This update better reflects the possibility of no metadata being found and ensures proper handling of undefined cases. +3. The `ReflectableDecorator`'s transformed type argument is now properly inferred across all methods. + +These enhancements improve the overall developer experience by providing better type safety and handling of metadata in NestJS 11. + +#### Lifecycle hooks execution order + +Termination lifecycle hooks are now executed in the reverse order to their initialization counterparts. That said, hooks like `OnModuleDestroy`, `BeforeApplicationShutdown`, and `OnApplicationShutdown` are now executed in the reverse order. + +Imagine the following scenario: + +```plaintext +// Where A, B, and C are modules and "->" represents the module dependency. +A -> B -> C +``` + +In this case, the `OnModuleInit` hooks are executed in the following order: + +```plaintext +C -> B -> A +``` + +While the `OnModuleDestroy` hooks are executed in the reverse order: + +```plaintext +A -> B -> C +``` + +> info **Hint** Global modules are treated as if they depend on all other modules. This means that global modules are initialized first and destroyed last. #### Cache module -The `CacheModule` has been removed from the `@nestjs/common` package and is now available as a standalone package - `@nestjs/cache-manager`. This change was made to avoid unnecessary dependencies in the `@nestjs/common` package. You can learn more about the `@nestjs/cache-manager` package [here](https://docs.nestjs.com/techniques/caching). +The `CacheModule` (from the `@nestjs/cache-manager` package) has been updated to support the latest version of the `cache-manager` package. This update brings a few breaking changes, including a migration to [Keyv](https://keyv.org/), which offers a unified interface for key-value storage across multiple backend stores through storage adapters. + +The key difference between the previous version and the new version lies in the configuration of external stores. In the previous version, to register a Redis store, you would have likely configured it like this: + +```ts +// Old version - no longer supported +CacheModule.registerAsync({ + useFactory: async () => { + const store = await redisStore({ + socket: { + host: 'localhost', + port: 6379, + }, + }); + + return { + store, + }; + }, +}), +``` + +In the new version, you should use the `Keyv` adapter to configure the store: + +```ts +// New version - supported +CacheModule.registerAsync({ + useFactory: async () => { + return { + stores: [ + new KeyvRedis('redis://localhost:6379'), + ], + }; + }, +}), +``` -#### Log levels +Where `KeyvRedis` is imported from the `@keyv/redis` package. See the [Caching documentation](/techniques/caching) to learn more. -Swapped the values for the `verbose` and `debug` log levels (see [[PR](https://github.com/nestjs/nest/pull/11036/files)](https://github.com/nestjs/nest/pull/11036/files)). The `debug` level is now set to `1`, while `verbose` is set to `0`. +#### Config module -#### Deprecations +If you're using the `ConfigModule` from the `@nestjs/config` package, be aware of several breaking changes introduced in `@nestjs/config@4.0.0`. Most notably, the order in which configuration variables are read by the `ConfigService#get` method has been updated. The new order is: -All deprecated methods & modules have been removed. +- Internal configuration (config namespaces and custom config files) +- Validated environment variables (if validation is enabled and a schema is provided) +- The `process.env` object -#### CLI Plugins and TypeScript >= 4.8 +Previously, validated environment variables and the `process.env` object were read first, preventing them from being overridden by internal configuration. With this update, internal configuration will now always take precedence over environment variables. -NestJS CLI Plugins (available for `@nestjs/swagger` and `@nestjs/graphql` packages) will now require TypeScript >= v4.8 and so older versions of TypeScript will no longer be supported. The reason for this change is that in [TypeScript v4.8 introduced several breaking changes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-8.html#decorators-are-placed-on-modifiers-on-typescripts-syntax-trees) in its Abstract Syntax Tree (AST) which we use to auto-generate OpenAPI and GraphQL schemas. +Additionally, the `ignoreEnvVars` configuration option, which previously allowed disabling validation of the `process.env` object, has been deprecated. Instead, use the `validatePredefined` option (set to `false` to disable validation of predefined environment variables). Predefined environment variables refer to `process.env` variables that were set before the module was imported. For example, if you start your application with `PORT=3000 node main.js`, the `PORT` variable is considered predefined. However, variables loaded by the `ConfigModule` from a `.env` file are not classified as predefined. -#### Dropping support for Node.js v12 +A new `skipProcessEnv` option has also been introduced. This option allows you to prevent the `ConfigService#get` method from accessing the `process.env` object entirely, which can be helpful when you want to restrict the service from reading environment variables directly. -As of NestJS 10, we no longer support Node.js v12, as [v12 went EOL](https://twitter.com/nodejs/status/1524081123579596800) on April 30, 2022. This means that NestJS 10 requires Node.js v16 or higher. This decision was made to allow us to finally set target to `ES2021` in our TypeScript configuration, instead of shipping polyfills as we did in the past. +#### Node.js v16 no longer supported -From now on, every official NestJS package will be compiled to `ES2021` by default, which should result in a smaller library size and sometimes even (slightly) better performance. +Starting with NestJS 11, Node.js v16 is no longer supported, as it reached its end-of-life (EOL) on September 11, 2023. NestJS 11 now requires **Node.js v20 or higher**. -We also strongly recommend using the latest LTS version. +To ensure the best experience, we strongly recommend using the latest LTS version of Node.js. diff --git a/content/pipes.md b/content/pipes.md index fa5e98d53b..ba7db0d9eb 100644 --- a/content/pipes.md +++ b/content/pipes.md @@ -19,7 +19,7 @@ Nest comes with a number of built-in pipes that you can use out-of-the-box. You #### Built-in pipes -Nest comes with nine pipes available out-of-the-box: +Nest comes with several pipes available out-of-the-box: - `ValidationPipe` - `ParseIntPipe` @@ -30,10 +30,11 @@ Nest comes with nine pipes available out-of-the-box: - `ParseEnumPipe` - `DefaultValuePipe` - `ParseFilePipe` +- `ParseDatePipe` They're exported from the `@nestjs/common` package. -Let's take a quick look at using `ParseIntPipe`. This is an example of the **transformation** use case, where the pipe ensures that a method handler parameter is converted to a JavaScript integer (or throws an exception if the conversion fails). Later in this chapter, we'll show a simple custom implementation for a `ParseIntPipe`. The example techniques below also apply to the other built-in transformation pipes (`ParseBoolPipe`, `ParseFloatPipe`, `ParseEnumPipe`, `ParseArrayPipe` and `ParseUUIDPipe`, which we'll refer to as the `Parse*` pipes in this chapter). +Let's take a quick look at using `ParseIntPipe`. This is an example of the **transformation** use case, where the pipe ensures that a method handler parameter is converted to a JavaScript integer (or throws an exception if the conversion fails). Later in this chapter, we'll show a simple custom implementation for a `ParseIntPipe`. The example techniques below also apply to the other built-in transformation pipes (`ParseBoolPipe`, `ParseFloatPipe`, `ParseEnumPipe`, `ParseArrayPipe`, `ParseDatePipe`, and `ParseUUIDPipe`, which we'll refer to as the `Parse*` pipes in this chapter). #### Binding pipes diff --git a/content/recipes/cqrs.md b/content/recipes/cqrs.md index 8553ff33de..ba8ce32912 100644 --- a/content/recipes/cqrs.md +++ b/content/recipes/cqrs.md @@ -24,6 +24,29 @@ First install the required package: $ npm install --save @nestjs/cqrs ``` +Once the installation is complete, navigate to the root module of your application (usually `AppModule`), and import the `CqrsModule.forRoot()`: + +```typescript +import { Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; + +@Module({ + imports: [CqrsModule.forRoot()], +}) +export class AppModule {} +``` + +This module accepts an optional configuration object. The following options are available: + +| Attribute | Description | Default | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `commandPublisher` | The publisher responsible for dispatching commands to the system. | `DefaultCommandPubSub` | +| `eventPublisher` | The publisher used to publish events, allowing them to be broadcasted or processed. | `DefaultPubSub` | +| `queryPublisher` | The publisher used for publishing queries, which can trigger data retrieval operations. | `DefaultQueryPubSub` | +| `unhandledExceptionPublisher` | Publisher responsible for handling unhandled exceptions, ensuring they are tracked and reported. | `DefaultUnhandledExceptionPubSub` | +| `eventIdProvider` | Service that provides unique event IDs by generating or retrieving them from event instances. | `DefaultEventIdProvider` | +| `rethrowUnhandled` | Determines whether unhandled exceptions should be rethrown after being processed, useful for debugging and error management. | `false` | + #### Commands Commands are used to change the application state. They should be task-based, rather than data centric. When a command is dispatched, it is handled by a corresponding **Command Handler**. The handler is responsible for updating the application state. @@ -60,14 +83,16 @@ In the code snippet above, we instantiate the `KillDragonCommand` class and pass ```typescript @@filename(kill-dragon.command) -export class KillDragonCommand { +export class KillDragonCommand extends Command<{ + actionId: string // This type represents the command execution result +}> { constructor( public readonly heroId: string, public readonly dragonId: string, ) {} } @@switch -export class KillDragonCommand { +export class KillDragonCommand extends Command { constructor(heroId, dragonId) { this.heroId = heroId; this.dragonId = dragonId; @@ -75,6 +100,10 @@ export class KillDragonCommand { } ``` +As you can see, the `KillDragonCommand` class extends the `Command` class. The `Command` class is a simple utility class exported from the `@nestjs/cqrs` package that lets you define the command's return type. In this case, the return type is an object with an `actionId` property. Now, whenever the `KillDragonCommand` command is dispatched, the `CommandBus#execute()` method return-type will be inferred as `Promise<{{ '{' }} actionId: string {{ '}' }}>`. This is useful when you want to return some data from the command handler. + +> info **Hint** Inheritance from the `Command` class is optional. It is only necessary if you want to define the return type of the command. + The `CommandBus` represents a **stream** of commands. It is responsible for dispatching commands to the appropriate handlers. The `execute()` method returns a promise, which resolves to the value returned by the handler. Let's create a handler for the `KillDragonCommand` command. @@ -91,6 +120,11 @@ export class KillDragonHandler implements ICommandHandler { hero.killEnemy(dragonId); await this.repository.persist(hero); + + // "ICommandHandler" forces you to return a value that matches the command's return type + return { + actionId: crypto.randomUUID(), // This value will be returned to the caller + } } } @@switch @@ -107,17 +141,78 @@ export class KillDragonHandler { hero.killEnemy(dragonId); await this.repository.persist(hero); + + // "ICommandHandler" forces you to return a value that matches the command's return type + return { + actionId: crypto.randomUUID(), // This value will be returned to the caller + } } } ``` This handler retrieves the `Hero` entity from the repository, calls the `killEnemy()` method, and then persists the changes. The `KillDragonHandler` class implements the `ICommandHandler` interface, which requires the implementation of the `execute()` method. The `execute()` method receives the command object as an argument. +Note that `ICommandHandler` forces you to return a value that matches the command's return type. In this case, the return type is an object with an `actionId` property. This only applies to commands that inherit from the `Command` class. Otherwise, you can return whatever you want. + +Lastly, make sure to register the `KillDragonHandler` as a provider in a module: + +```typescript +providers: [KillDragonHandler]; +``` + #### Queries Queries are used to retrieve data from the application state. They should be data centric, rather than task-based. When a query is dispatched, it is handled by a corresponding **Query Handler**. The handler is responsible for retrieving the data. -The `QueryBus` follows the same pattern as the `CommandBus`. Query handlers should implement the `IQueryHandler` interface and be annotated with the `@QueryHandler()` decorator. +The `QueryBus` follows the same pattern as the `CommandBus`. Query handlers should implement the `IQueryHandler` interface and be annotated with the `@QueryHandler()` decorator. See the following example: + +```typescript +export class GetHeroQuery extends Query { + constructor(public readonly heroId: string) {} +} +``` + +Similar to the `Command` class, the `Query` class is a simple utility class exported from the `@nestjs/cqrs` package that lets you define the query's return type. In this case, the return type is a `Hero` object. Now, whenever the `GetHeroQuery` query is dispatched, the `QueryBus#execute()` method return-type will be inferred as `Promise`. + +To retrieve the hero, we need to create a query handler: + +```typescript +@@filename(get-hero.handler) +@QueryHandler(GetHeroQuery) +export class GetHeroHandler implements IQueryHandler { + constructor(private repository: HeroRepository) {} + + async execute(query: GetHeroQuery) { + return this.repository.findOneById(query.hero); + } +} +@@switch +@QueryHandler(GetHeroQuery) +@Dependencies(HeroRepository) +export class GetHeroHandler { + constructor(repository) { + this.repository = repository; + } + + async execute(query) { + return this.repository.findOneById(query.hero); + } +} +``` + +The `GetHeroHandler` class implements the `IQueryHandler` interface, which requires the implementation of the `execute()` method. The `execute()` method receives the query object as an argument, and must return the data that matches the query's return type (in this case, a `Hero` object). + +Lastly, make sure to register the `GetHeroHandler` as a provider in a module: + +```typescript +providers: [GetHeroHandler]; +``` + +Now, to dispatch the query, use the `QueryBus`: + +```typescript +const hero = await this.queryBus.execute(new GetHeroQuery(heroId)); // "hero" will be auto-inferred as "Hero" type +``` #### Events @@ -261,6 +356,12 @@ export class HeroKilledDragonHandler implements IEventHandler - HTTP Responses in `CommandHandlers` can still be sent back to the client. > - HTTP Responses in `EventHandlers` cannot. If you want to send information to the client you could use [WebSocket](/websockets/gateways), [SSE](/techniques/server-sent-events), or whatever other solution you choose. +As with commands and queries, make sure to register the `HeroKilledDragonHandler` as a provider in a module: + +```typescript +providers: [HeroKilledDragonHandler]; +``` + #### Sagas Saga is a long-running process that listens to events and may trigger new commands. It is usually used to manage complex workflows in the application. For example, when a user signs up, a saga may listen to the `UserRegisteredEvent` and send a welcome email to the user. @@ -300,32 +401,15 @@ The `@Saga()` decorator marks the method as a saga. The `events$` argument is an In this example, we map the `HeroKilledDragonEvent` to the `DropAncientItemCommand` command. The `DropAncientItemCommand` command is then auto-dispatched by the `CommandBus`. -#### Setup - -To wrap up, we need to register all command handlers, event handlers, and sagas in the `HeroesGameModule`: +As with query, command, and event handlers, make sure to register the `HeroesGameSagas` as a provider in a module: ```typescript -@@filename(heroes-game.module) -export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler]; -export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler]; - -@Module({ - imports: [CqrsModule], - controllers: [HeroesGameController], - providers: [ - HeroesGameService, - HeroesGameSagas, - ...CommandHandlers, - ...EventHandlers, - HeroRepository, - ] -}) -export class HeroesGameModule {} +providers: [HeroesGameSagas]; ``` #### Unhandled exceptions -Event handlers are executed in the asynchronous manner. This means they should always handle all exceptions to prevent application from entering the inconsistent state. However, if an exception is not handled, the `EventBus` will create the `UnhandledExceptionInfo` object and push it to the `UnhandledExceptionBus` stream. This stream is an `Observable` which can be used to process unhandled exceptions. +Event handlers are executed asynchronously, so they must always handle exceptions properly to prevent the application from entering an inconsistent state. If an exception is not handled, the `EventBus` will create an `UnhandledExceptionInfo` object and push it to the `UnhandledExceptionBus` stream. This stream is an `Observable` that can be used to process unhandled exceptions. ```typescript private destroy$ = new Subject(); @@ -348,9 +432,14 @@ onModuleDestroy() { To filter out exceptions, we can use the `ofType` operator, as follows: ```typescript -this.unhandledExceptionsBus.pipe(takeUntil(this.destroy$), UnhandledExceptionBus.ofType(TransactionNotAllowedException)).subscribe((exceptionInfo) => { - // Handle exception here -}); +this.unhandledExceptionsBus + .pipe( + takeUntil(this.destroy$), + UnhandledExceptionBus.ofType(TransactionNotAllowedException), + ) + .subscribe((exceptionInfo) => { + // Handle exception here + }); ``` Where `TransactionNotAllowedException` is the exception we want to filter out. @@ -358,7 +447,10 @@ Where `TransactionNotAllowedException` is the exception we want to filter out. The `UnhandledExceptionInfo` object contains the following properties: ```typescript -export interface UnhandledExceptionInfo { +export interface UnhandledExceptionInfo< + Cause = IEvent | ICommand, + Exception = any, +> { /** * The exception that was thrown. */ @@ -391,6 +483,120 @@ onModuleDestroy() { } ``` +#### Request-scoping + +For those coming from different programming language backgrounds, it may be surprising to learn that in Nest, most things are shared across incoming requests. This includes a connection pool to the database, singleton services with global state, and more. Keep in mind that Node.js does not follow the request/response multi-threaded stateless model, where each request is processed by a separate thread. As a result, using singleton instances is **safe** for our applications. + +However, there are edge cases where a request-based lifetime for the handler might be desirable. This could include scenarios like per-request caching in GraphQL applications, request tracking, or multi-tenancy. You can learn more about how to control scopes [here](/fundamentals/injection-scopes). + +Using request-scoped providers alongside CQRS can be complex because the `CommandBus`, `QueryBus`, and `EventBus` are singletons. Thankfully, the `@nestjs/cqrs` package simplifies this by automatically creating a new instance of request-scoped handlers for each processed command, query, or event. + +To make a handler request-scoped, you can either: + +1. Depend on a request-scoped provider. +2. Explicitly set its scope to `REQUEST` using the `@CommandHandler`, `@QueryHandler`, or `@EventHandler` decorator, as shown: + +```typescript +@CommandHandler(KillDragonCommand, { + scope: Scope.REQUEST, +}) +export class KillDragonHandler { + // Implementation here +} +``` + +To inject the request payload into any request-scoped provider, you use the `@Inject(REQUEST)` decorator. However, the nature of the request payload in CQRS depends on the context—it could be an HTTP request, a scheduled job, or any other operation that triggers a command. + +The payload must be an instance of a class extending `AsyncContext` (provided by `@nestjs/cqrs`), which acts as the request context and holds data accessible throughout the request lifecycle. + +```typescript +import { AsyncContext } from '@nestjs/cqrs'; + +export class MyRequest extends AsyncContext { + constructor(public readonly user: User) { + super(); + } +} +``` + +When executing a command, pass the custom request context as the second argument to the `CommandBus#execute` method: + +```typescript +const myRequest = new MyRequest(user); +await this.commandBus.execute( + new KillDragonCommand(heroId, killDragonDto.dragonId), + myRequest, +); +``` + +This makes the `MyRequest` instance available as the `REQUEST` provider to the corresponding handler: + +```typescript +@CommandHandler(KillDragonCommand, { + scope: Scope.REQUEST, +}) +export class KillDragonHandler { + constructor( + @Inject(REQUEST) private request: MyRequest, // Inject the request context + ) {} + + // Handler implementation here +} +``` + +You can follow the same approach for queries: + +```typescript +const myRequest = new MyRequest(user); +const hero = await this.queryBus.execute(new GetHeroQuery(heroId), myRequest); +``` + +And in the query handler: + +```typescript +@QueryHandler(GetHeroQuery, { + scope: Scope.REQUEST, +}) +export class GetHeroHandler { + constructor( + @Inject(REQUEST) private request: MyRequest, // Inject the request context + ) {} + + // Handler implementation here +} +``` + +For events, while you can pass the request provider to `EventBus#publish`, this is less common. Instead, use `EventPublisher` to merge the request provider into a model: + +```typescript +const hero = this.publisher.mergeObjectContext( + await this.repository.findOneById(+heroId), + this.request, // Inject the request context here +); +``` + +Request-scoped event handlers subscribing to these events will have access to the request provider. + +Sagas are always singleton instances because they manage long-running processes. However, you can retrieve the request provider from event objects: + +```typescript +@Saga() +dragonKilled = (events$: Observable): Observable => { + return events$.pipe( + ofType(HeroKilledDragonEvent), + map((event) => { + const request = AsyncContext.of(event); // Retrieve the request context + const command = new DropAncientItemCommand(event.heroId, fakeItemID); + + AsyncContext.merge(request, command); // Merge the request context into the command + return command; + }), + ); +} +``` + +Alternatively, use the `request.attachTo(command)` method to tie the request context to the command. + #### Example A working example is available [here](https://github.com/kamilmysliwiec/nest-cqrs-example). diff --git a/content/techniques/caching.md b/content/techniques/caching.md index e024a1f59e..a887a90355 100644 --- a/content/techniques/caching.md +++ b/content/techniques/caching.md @@ -1,26 +1,20 @@ ### Caching -Caching is a great and simple **technique** that helps improve your app's performance. It acts as a temporary data store providing high performance data access. +Caching is a powerful and straightforward **technique** for enhancing your application's performance. By acting as a temporary storage layer, it allows for quicker access to frequently used data, reducing the need to repeatedly fetch or compute the same information. This results in faster response times and improved overall efficiency. #### Installation -First install required packages: +To get started with caching in Nest, you need to install the `@nestjs/cache-manager` package along with the `cache-manager` package. ```bash $ npm install @nestjs/cache-manager cache-manager ``` -> warning **Warning** `cache-manager` version 4 uses seconds for `TTL (Time-To-Live)`. The current version of `cache-manager` (v5) has switched to using milliseconds instead. NestJS doesn't convert the value, and simply forwards the ttl you provide to the library. In other words: -> -> - If using `cache-manager` v4, provide ttl in seconds -> - If using `cache-manager` v5, provide ttl in milliseconds -> - Documentation is referring to seconds, since NestJS was released targeting version 4 of cache-manager. +By default, everything is stored in memory; Since `cache-manager` uses [Keyv](https://keyv.org/docs/) under the hood, you can easily switch to a more advanced storage solution, such as Redis, by installing the appropriate package. We'll cover this in more detail later. #### In-memory cache -Nest provides a unified API for various cache storage providers. The built-in one is an in-memory data store. However, you can easily switch to a more comprehensive solution, like Redis. - -In order to enable caching, import the `CacheModule` and call its `register()` method. +To enable caching in your application, import the `CacheModule` and configure it using the `register()` method: ```typescript import { Module } from '@nestjs/common'; @@ -34,6 +28,8 @@ import { AppController } from './app.controller'; export class AppModule {} ``` +This setup initializes in-memory caching with default settings, allowing you to start caching data immediately. + #### Interacting with the Cache store To interact with the cache manager instance, inject it to your class using the `CACHE_MANAGER` token, as follows: @@ -227,30 +223,35 @@ class HttpCacheInterceptor extends CacheInterceptor { } ``` -#### Different stores +#### Using alternative Cache stores + +Switching to a different cache store is straightforward. First, install the appropriate package. For example, to use Redis, install the `@keyv/redis` package: + +```bash +$ npm install @keyv/redis +``` -The `cache-manager` package offers a variety of useful storage options, including the [Redis store](https://www.npmjs.com/package/cache-manager-redis-yet), which is the official package for integrating Redis with cache-manager. You can find a comprehensive list of supported stores [here](https://github.com/jaredwray/cacheable/blob/main/packages/cache-manager/READMEv5.md#store-engines). To configure the Redis store, use the `registerAsync()` method to initialize it, as shown below: +With this in place, you can register the `CacheModule` with multiple stores as shown below: ```typescript -import { redisStore } from 'cache-manager-redis-yet'; import { Module } from '@nestjs/common'; import { CacheModule, CacheStore } from '@nestjs/cache-manager'; import { AppController } from './app.controller'; +import KeyvRedis from '@keyv/redis'; +import { Keyv } from 'keyv'; +import { CacheableMemory } from 'cacheable'; @Module({ imports: [ CacheModule.registerAsync({ useFactory: async () => { - const store = await redisStore({ - socket: { - host: 'localhost', - port: 6379, - }, - }); - return { - store: store as unknown as CacheStore, - ttl: 3 * 60000, // 3 minutes (milliseconds) + stores: [ + new Keyv({ + store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }), + }), + new KeyvRedis('redis://localhost:6379'), + ], }; }, }), @@ -260,7 +261,9 @@ import { AppController } from './app.controller'; export class AppModule {} ``` -> warning **Warning** The `cache-manager-redis-yet` package requires the `ttl` (time-to-live) setting to be specified directly rather than as part of the module options. +In this example, we've registered two stores: `CacheableMemory` and `KeyvRedis`. The `CacheableMemory` store is a simple in-memory store, while `KeyvRedis` is a Redis store. The `stores` array is used to specify the stores you want to use. The first store in the array is the default store, and the rest are fallback stores. + +Check out the [Keyv documentation](https://keyv.org/docs/) for more information on available stores. #### Async configuration diff --git a/content/techniques/configuration.md b/content/techniques/configuration.md index 09226f1efa..c0574bd8e1 100644 --- a/content/techniques/configuration.md +++ b/content/techniques/configuration.md @@ -44,6 +44,12 @@ DATABASE_USER=test DATABASE_PASSWORD=test ``` +If you need some env variables to be available even before the `ConfigModule` is loaded and Nest application is bootstrapped (for example, to pass the microservice configuration to the `NestFactory#createMicroservice` method), you can use the `--env-file` option of the Nest CLI. This option allows you to specify the path to the `.env` file that should be loaded before the application starts. `--env-file` flag support was introduced in Node v20, see [the documentation](https://nodejs.org/dist/v20.18.1/docs/api/cli.html#--env-fileconfig) for more details. + +```bash +$ nest start --env-file .env +``` + #### Custom env file path By default, the package looks for a `.env` file in the root directory of the application. To specify another path for the `.env` file, set the `envFilePath` property of an (optional) options object you pass to `forRoot()`, as follows: @@ -277,6 +283,8 @@ constructor(private configService: ConfigService<{ PORT: number }, true>) { } ``` +> info **Hint** To make sure the `ConfigService#get` method retrieves values exclusively from custom configuration files and ignores `process.env` variables, set the `skipProcessEnv` option to `true` in the options object of the `ConfigModule`'s `forRoot()` method. + #### Configuration namespaces The `ConfigModule` allows you to define and load multiple custom configuration files, as shown in Custom configuration files above. You can manage complex configuration object hierarchies with nested configuration objects as shown in that section. Alternatively, you can return a "namespaced" configuration object with the `registerAs()` function as follows: @@ -447,6 +455,8 @@ The `@nestjs/config` package uses default settings of: Note that once you decide to pass a `validationOptions` object, any settings you do not explicitly pass will default to `Joi` standard defaults (not the `@nestjs/config` defaults). For example, if you leave `allowUnknowns` unspecified in your custom `validationOptions` object, it will have the `Joi` default value of `false`. Hence, it is probably safest to specify **both** of these settings in your custom object. +> info **Hint** To disable validation of predefined environment variables, set the `validatePredefined` attribute to `false` in the `forRoot()` method's options object. Predefined environment variables are process variables (`process.env` variables) that were set before the module was imported. For example, if you start your application with `PORT=3000 node main.js`, then `PORT` is a predefined environment variable. + #### Custom validate function Alternatively, you can specify a **synchronous** `validate` function that takes an object containing the environment variables (from env file and process) and returns an object containing validated environment variables so that you can convert/mutate them if needed. If the function throws an error, it will prevent the application from bootstrapping. diff --git a/content/techniques/logger.md b/content/techniques/logger.md index f5ee6a9412..5e1752ecf1 100644 --- a/content/techniques/logger.md +++ b/content/techniques/logger.md @@ -4,6 +4,7 @@ Nest comes with a built-in text-based logger which is used during application bo - disable logging entirely - specify the log level of detail (e.g., display errors, warnings, debug information, etc.) +- configure formatting of log messages (raw, json, colorized, etc.) - override timestamp in the default logger (e.g., use ISO8601 standard as date format) - completely override the default logger - customize the default logger by extending it @@ -11,7 +12,7 @@ Nest comes with a built-in text-based logger which is used during application bo You can also make use of the built-in logger, or create your own custom implementation, to log your own application-level events and messages. -For more advanced logging functionality, you can make use of any Node.js logging package, such as [Winston](https://github.com/winstonjs/winston), to implement a completely custom, production grade logging system. +If your application requires integration with external logging systems, automatic file-based logging, or forwarding logs to a centralized logging service, you can implement a fully custom logging solution using a Node.js logging library. One popular choice is [Pino](https://github.com/pinojs/pino), known for its high performance and flexibility. #### Basic customization @@ -35,7 +36,134 @@ await app.listen(process.env.PORT ?? 3000); Values in the array can be any combination of `'log'`, `'fatal'`, `'error'`, `'warn'`, `'debug'`, and `'verbose'`. -> info **Hint** To disable color in the default logger's messages, set the `NO_COLOR` environment variable to some non-empty string. +To disable colorized output, pass the `ConsoleLogger` object with the `colors` property set to `false` as the value of the `logger` property. + +```typescript +const app = await NestFactory.create(AppModule, { + logger: new ConsoleLogger({ + colors: false, + }), +}); +``` + +To configure a prefix for each log message, pass the `ConsoleLogger` object with the `prefix` attribute set: + +```typescript +const app = await NestFactory.create(AppModule, { + logger: new ConsoleLogger({ + prefix: 'MyApp', // Default is "Nest" + }), +}); +``` + +Here are all the available options listed in the table below: + +| Option | Description | Default | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| `logLevels` | Enabled log levels. | `['log', 'error', 'warn', 'debug', 'verbose']` | +| `timestamp` | If enabled, will print timestamp (time difference) between current and previous log message. Note: This option is not used when `json` is enabled. | `false` | +| `prefix` | A prefix to be used for each log message. Note: This option is not used when `json` is enabled. | `Nest` | +| `json` | If enabled, will print the log message in JSON format. | `false` | +| `colors` | If enabled, will print the log message in color. Default true if json is disabled, false otherwise. | `true` | +| `context` | The context of the logger. | `undefined` | +| `compact` | If enabled, will print the log message in a single line, even if it is an object with multiple properties. If set to a number, the most n inner elements are united on a single line as long as all properties fit into breakLength. Short array elements are also grouped together. | `true` | +| `maxArrayLength` | Specifies the maximum number of Array, TypedArray, Map, Set, WeakMap, and WeakSet elements to include when formatting. Set to null or Infinity to show all elements. Set to 0 or negative to show no elements. Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output. | `100` | +| `maxStringLength` | Specifies the maximum number of characters to include when formatting. Set to null or Infinity to show all elements. Set to 0 or negative to show no characters. Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output. | `10000` | +| `sorted` | If enabled, will sort keys while formatting objects. Can also be a custom sorting function. Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output. | `false` | +| `depth` | Specifies the number of times to recurse while formatting object. This is useful for inspecting large objects. To recurse up to the maximum call stack size pass Infinity or null. Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output. | `5` | +| `showHidden` | If true, object's non-enumerable symbols and properties are included in the formatted result. WeakMap and WeakSet entries are also included as well as user defined prototype properties | `false` | +| `breakLength` | The length at which input values are split across multiple lines. Set to Infinity to format the input as a single line (in combination with "compact" set to true). Default Infinity when "compact" is true, 80 otherwise. Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output. | `Infinity` | + +#### JSON logging + +JSON logging is essential for modern application observability and integration with log management systems. To enable JSON logging in your NestJS application, configure the `ConsoleLogger` object with its `json` property set to `true`. Then, provide this logger configuration as the value for the `logger` property when creating the application instance. + +```typescript +const app = await NestFactory.create(AppModule, { + logger: new ConsoleLogger({ + json: true, + }), +}); +``` + +This configuration outputs logs in a structured JSON format, making it easier to integrate with external systems such as log aggregators and cloud platforms. For example, platforms like **AWS ECS** (Elastic Container Service) natively support JSON logs, enabling advanced features like: + +- **Log Filtering**: Easily narrow down logs based on fields like log level, timestamp, or custom metadata. +- **Search and Analysis**: Use query tools to analyze and track trends in your application's behavior. + +Additionally, if you're using [NestJS Mau](https://mau.nestjs.com), JSON logging simplifies the process of viewing logs in a well-organized, structured format, which is especially useful for debugging and performance monitoring. + +> info **Note** When `json` is set to `true`, the `ConsoleLogger` automatically disables text colorization by setting the `colors` property to `false`. This ensures that the output remains valid JSON, free of formatting artifacts. However, for development purposes, you can override this behavior by explicitly setting `colors` to `true`. This adds colorized JSON logs, which can make log entries more readable during local debugging. + +When JSON logging is enabled, the log output will look like this (in a single line): + +```json +{ + "level": "log", + "pid": 19096, + "timestamp": 1607370779834, + "message": "Starting Nest application...", + "context": "NestFactory" +} +``` + +You can see different variants in this [Pull Request](https://github.com/nestjs/nest/pull/14121). + +#### Using the logger for application logging + +We can combine several of the techniques above to provide consistent behavior and formatting across both Nest system logging and our own application event/message logging. + +A good practice is to instantiate `Logger` class from `@nestjs/common` in each of our services. We can supply our service name as the `context` argument in the `Logger` constructor, like so: + +```typescript +import { Logger, Injectable } from '@nestjs/common'; + +@Injectable() +class MyService { + private readonly logger = new Logger(MyService.name); + + doSomething() { + this.logger.log('Doing something...'); + } +} +``` + +In the default logger implementation, `context` is printed in the square brackets, like `NestFactory` in the example below: + +```bash +[Nest] 19096 - 12/08/2019, 7:12:59 AM [NestFactory] Starting Nest application... +``` + +If we supply a custom logger via `app.useLogger()`, it will actually be used by Nest internally. That means that our code remains implementation agnostic, while we can easily substitute the default logger for our custom one by calling `app.useLogger()`. + +That way if we follow the steps from the previous section and call `app.useLogger(app.get(MyLogger))`, the following calls to `this.logger.log()` from `MyService` would result in calls to method `log` from `MyLogger` instance. + +This should be suitable for most cases. But if you need more customization (like adding and calling custom methods), move to the next section. + +#### Logs with timestamps + +To enable timestamp logging for every logged message, you can use the optional `timestamp: true` setting when creating the logger instance. + +```typescript +import { Logger, Injectable } from '@nestjs/common'; + +@Injectable() +class MyService { + private readonly logger = new Logger(MyService.name, { timestamp: true }); + + doSomething() { + this.logger.log('Doing something with timestamp here ->'); + } +} +``` + +This will produce output in the following format: + +```bash +[Nest] 19096 - 04/19/2024, 7:12:59 AM [MyService] Doing something with timestamp here +5ms +``` + +Note the `+5ms` at the end of the line. For each log statement, the time difference from the previous message is calculated and displayed at the end of the line. #### Custom implementation @@ -157,62 +285,6 @@ Here we use the `get()` method on the `NestApplication` instance to retrieve the You can also inject this `MyLogger` provider in your feature classes, thus ensuring consistent logging behavior across both Nest system logging and application logging. See Using the logger for application logging and Injecting a custom logger below for more information. -#### Using the logger for application logging - -We can combine several of the techniques above to provide consistent behavior and formatting across both Nest system logging and our own application event/message logging. - -A good practice is to instantiate `Logger` class from `@nestjs/common` in each of our services. We can supply our service name as the `context` argument in the `Logger` constructor, like so: - -```typescript -import { Logger, Injectable } from '@nestjs/common'; - -@Injectable() -class MyService { - private readonly logger = new Logger(MyService.name); - - doSomething() { - this.logger.log('Doing something...'); - } -} -``` - -In the default logger implementation, `context` is printed in the square brackets, like `NestFactory` in the example below: - -```bash -[Nest] 19096 - 12/08/2019, 7:12:59 AM [NestFactory] Starting Nest application... -``` - -If we supply a custom logger via `app.useLogger()`, it will actually be used by Nest internally. That means that our code remains implementation agnostic, while we can easily substitute the default logger for our custom one by calling `app.useLogger()`. - -That way if we follow the steps from the previous section and call `app.useLogger(app.get(MyLogger))`, the following calls to `this.logger.log()` from `MyService` would result in calls to method `log` from `MyLogger` instance. - -This should be suitable for most cases. But if you need more customization (like adding and calling custom methods), move to the next section. - -#### Logs with timestamps - -To enable timestamp logging for every logged message, you can use the optional `timestamp: true` setting when creating the logger instance. - -```typescript -import { Logger, Injectable } from '@nestjs/common'; - -@Injectable() -class MyService { - private readonly logger = new Logger(MyService.name, { timestamp: true }); - - doSomething() { - this.logger.log('Doing something with timestamp here ->'); - } -} -``` - -This will produce output in the following format: - -```bash -[Nest] 19096 - 04/19/2024, 7:12:59 AM [MyService] Doing something with timestamp here +5ms -``` - -Note the `+5ms` at the end of the line. For each log statement, the time difference from the previous message is calculated and displayed at the end of the line. - #### Injecting a custom logger To start, extend the built-in logger with code like the following. We supply the `scope` option as configuration metadata for the `ConsoleLogger` class, specifying a [transient](/fundamentals/injection-scopes) scope, to ensure that we'll have a unique instance of the `MyLogger` in each feature module. In this example, we do not extend the individual `ConsoleLogger` methods (like `log()`, `warn()`, etc.), though you may choose to do so. diff --git a/content/websockets/adapter.md b/content/websockets/adapter.md index b5bdc32664..bceffb6cac 100644 --- a/content/websockets/adapter.md +++ b/content/websockets/adapter.md @@ -94,6 +94,20 @@ app.useWebSocketAdapter(new WsAdapter(app)); > info **Hint** The `WsAdapter` is imported from `@nestjs/platform-ws`. +The `wsAdapter` is designed to handle messages in the `{{ '{' }} event: string, data: any {{ '}' }}` format. If you need to receive and process messages in a different format, you'll need to configure a message parser to convert them into this required format. + +```typescript +const wsAdapter = new WsAdapter(app, { + // To handle messages in the [event, data] format + messageParser: (data) => { + const [event, payload] = JSON.parse(data.toString()); + return { event, data: payload }; + }, +}); +``` + +Alternatively, you can configure the message parser after the adapter is created by using the `setMessageParser` method. + #### Advanced (custom adapter) For demonstration purposes, we are going to integrate the [ws](https://github.com/websockets/ws) library manually. As mentioned, the adapter for this library is already created and is exposed from the `@nestjs/platform-ws` package as a `WsAdapter` class. Here is how the simplified implementation could potentially look like: