Nest JS Understanding power of RX Observables

·

13 min read

Nest JS Understanding power of RX Observables

What is NestJS?

As described in the Nestjs website, Nestjs is a progressive Node.js framework for building efficient, reliable and scalable server-side applications.

Github Link github.com/tkssharma/blogs/tree/master/nest..

Nestjs combines the best programming practice and the cutting-edge techniques from the NodeJS communities.

  • A lot of NestJS concepts are heavily inspired by the effort of the popular frameworks in the world, esp. Angular .
  • Nestjs hides the complexities of web programming in NodeJS, it provides a common abstraction of the web request handling, you are free to choose Expressjs or Fastify as the background engine.
  • Nestjs provides a lot of third party project integrations, from database operations, such as Mongoose, TypeORM, etc. to Message Brokers, such as Redis, RabbitMQ, etc.

If you are new to Nestjs like me but has some experience of Angular , TypeDI or Spring WebMVC, bootstraping a Nestjs project is really a piece of cake.

Generating a NestJS project

Make sure you have installed the latest Nodejs.

npm i -g @nestjs/cli

When it is finished, there is a nest command available in the Path. The usage of nest is similar with ng (Angular CLI), type nest --help in the terminal to list help for all commands.

❯ nest --help
Usage: nest <command> [options]

Options:
  -v, --version                                   Output the current version.
  -h, --help                                      Output usage information.

Commands:
  new|n [options] [name]                          Generate Nest application.
  build [options] [app]                           Build Nest application.
  start [options] [app]                           Run Nest application.
  info|i                                          Display Nest project details.
  update|u [options]                              Update Nest dependencies.
  add [options] <library>                         Adds support for an external library to your project.
  generate|g [options] <schematic> [name] [path]  Generate a Nest element.
    Available schematics:
      ┌───────────────┬─────────────┐
      │ name          │ alias       │
      │ application   │ application │
      │ class         │ cl          │
      │ configuration │ config      │
      │ controller    │ co          │
      │ decorator     │ d           │
      │ filter        │ f           │
      │ gateway       │ ga          │
      │ guard         │ gu          │
      │ interceptor   │ in          │
      │ interface     │ interface   │
      │ middleware    │ mi          │
      │ module        │ mo          │
      │ pipe          │ pi          │
      │ provider      │ pr          │
      │ resolver      │ r           │
      │ service       │ s           │
      │ library       │ lib         │
      │ sub-app       │ app         │
      └───────────────┴─────────────┘

Now generate a Nestjs project via:

nest new nestjs-sample

Open it in your favorite IDEs, such as Intellij WebStorm or VSCode.

Exploring the project files

Expand the project root, you will see the following like tree nodes.

.
├── LICENSE
├── nest-cli.json
├── package.json
├── package-lock.json
├── README.md
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

The default structure of this project is very similar with the one generated by Angular CLI.

  • src/main.ts is the entry file of this application.
  • src/app* is the top level component in a nest application.
    • There is an app.module.ts is a Nestjs Module which is similar with Angular NgModule, and used to organize codes in the logic view.
    • The app.service.ts is an @Injectable component, similar with the service in Angular or Spring's Service, it is used for handling business logic. A service is annotated with @Injectable.
    • The app.controller.ts is the controller of MVC, to handle incoming request, and responds the handled result back to client. The annotatoin @Controller() is similar with Spring MVC's @Controller.
    • The app.controller.spec.ts is test file for app.controller.ts. Nestjs uses Jest as testing framework.
  • test folder is for storing e2e test files.

We can simple Controller and service in the sample code that looks something like this

import { Get, Post, Body, Put, Delete, Param, Controller, UsePipes } from '@nestjs/common';
import { Request } from 'express';
import { UserService } from './user.service';
import { UserRO } from './user.interface';
import { CreateUserDto, UpdateUserDto, LoginUserDto } from './dto';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { User } from './user.decorator';
import { ValidationPipe } from '../shared/pipes/validation.pipe';

import {
  ApiBearerAuth, ApiTags
} from '@nestjs/swagger';

@ApiBearerAuth()
@ApiTags('user')
@Controller()
export class UserController {

  constructor(private readonly userService: UserService) {}

  @Get('user')
  async findMe(@User('email') email: string): Promise<UserRO> {
    return await this.userService.findByEmail(email);
  }

  @Put('user')
  async update(@User('id') userId: number, @Body('user') userData: UpdateUserDto) {
    return await this.userService.update(userId, userData);
  }
}

User service

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository, DeleteResult } from 'typeorm';
import { UserEntity } from './user.entity';
import {CreateUserDto, LoginUserDto, UpdateUserDto} from './dto';
const jwt = require('jsonwebtoken');
import { SECRET } from '../config';
import { UserRO } from './user.interface';
import { validate } from 'class-validator';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { HttpStatus } from '@nestjs/common';
import * as argon2 from 'argon2';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>
  ) {}

  async findAll(): Promise<UserEntity[]> {
    return await this.userRepository.find();
  }

  async findOne({email, password}: LoginUserDto): Promise<UserEntity> {
    const user = await this.userRepository.findOne({email});
    if (!user) {
      return null;
    }

    if (await argon2.verify(user.password, password)) {
      return user;
    }

    return null;
  }
}

And Finally main Module

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ArticleModule } from './article/article.module';
import { UserModule } from './user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { ProfileModule } from './profile/profile.module';
import { TagModule } from './tag/tag.module';

@Module({
  imports: [
    TypeOrmModule.forRoot(),
    ArticleModule,
    UserModule,
    ProfileModule,
    TagModule
  ],
  controllers: [
    AppController
  ],
  providers: []
})
export class ApplicationModule {
  constructor(private readonly connection: Connection) {}
}

This is sample how we write the controller and service now lets see how can we do same using observable and RX JS Operators

RxJS (Reactive Extensions for JavaScript) is a library that provides a set of powerful tools for reactive programming in JavaScript. It is based on the concept of Observables, which are a way to handle asynchronous data streams and perform operations on those streams in a functional and declarative manner.

In traditional programming, you typically write imperative code that explicitly specifies how to perform actions and handle data. However, with RxJS and the concept of Observables, you can write code that reacts to changes in data and events, allowing you to build more flexible and responsive applications.

Observables represent data streams over time. They can emit multiple values, including asynchronous data such as HTTP responses, events, or user input. Observables can be created from various sources such as arrays, events, timers, or even custom sources.

Observables provide a wide range of operators that allow you to transform, filter, combine, and manipulate the data streams. These operators follow functional programming principles and enable you to create complex data processing pipelines with ease. Some common operators include map, filter, reduce, merge, and debounce, among many others.

One of the key benefits of using Observables is that they support composition. You can combine multiple observables, apply operators to them, and create new observables as a result. This composability makes it easy to build complex asynchronous workflows and handle data dependencies.

Another important aspect of Observables is that they support handling errors and completion. Observables can emit error notifications when something goes wrong during data processing, allowing you to handle and recover from errors gracefully. They also emit a completion notification when the stream of data has ended, indicating that no more values will be emitted.

Overall, RxJS and Observables provide a powerful and expressive way to handle asynchronous and event-based programming in JavaScript. They enable you to write code that is more reactive, declarative, and efficient when dealing with complex asynchronous scenarios, making it easier to manage and process data streams in a predictable and composable way.

Using RX JS with Nest JS Project

Using RxJS with a NestJS application can enhance its capabilities for handling asynchronous operations and creating reactive pipelines. Here are some ways you can integrate RxJS into your NestJS application:

  1. Asynchronous Operations: NestJS applications often involve interacting with external services or performing time-consuming operations. RxJS can help manage these asynchronous tasks. You can use Observables to represent asynchronous data streams and apply operators to handle data processing. For example, you can use the from operator to convert promises or callback-based functions into Observables.

  2. Reactive Controllers: NestJS controllers handle incoming requests and generate responses. By leveraging RxJS, you can create reactive endpoints that respond to changes in data or events. You can use Observables to represent data streams, apply operators to transform the data, and then return the result as a response.

  3. Inter-service Communication: In a microservices architecture, NestJS applications may need to communicate with other services. RxJS can facilitate this communication by using Observables as a means of streaming data between services. You can use operators like switchMap or mergeMap to handle data dependencies and make multiple service calls in a reactive manner.

  4. Middleware and Pipes: NestJS provides middleware and pipes for intercepting and modifying incoming requests and outgoing responses. You can use RxJS operators to handle asynchronous operations within middleware or pipes. For example, you can use the map operator to transform the data or the catchError operator to handle errors.

  5. Event-driven Programming: NestJS applications can benefit from event-driven programming, where components react to events and trigger actions accordingly. RxJS provides a rich set of operators to handle event streams. You can use subjects or event emitters as Observables to represent events and use operators like filter or debounceTime to handle event stream transformations.

  6. Testing: RxJS offers testing utilities that can be used to write unit tests for NestJS applications. You can use operators like toArray or toPromise to convert Observables into arrays or promises, allowing you to assert the emitted values during testing.

Remember to import the necessary RxJS modules, such as Observable and required operators, in your NestJS components to start using RxJS functionality effectively within your application.

By integrating RxJS into your NestJS application, you can harness the power of reactive programming, handle asynchronous operations gracefully, and create responsive, event-driven architectures.

Certainly! Here's an example code snippet that demonstrates how to use RxJS within a NestJS application:

import { Controller, Get } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Controller('example')
export class ExampleController {
  @Get()
  getData(): Observable<any> {
    return this.fetchData().pipe(
      map(data => {
        // Transform the data using RxJS operators
        return this.processData(data);
      }),
    );
  }

  private fetchData(): Observable<any> {
    // Simulate an asynchronous data retrieval
    return new Observable(observer => {
      setTimeout(() => {
        observer.next('Example data');
        observer.complete();
      }, 1000);
    });
  }

  private processData(data: any): any {
    // Perform some processing on the data
    return data.toUpperCase();
  }
}

In the above example, we have a simple NestJS controller with a single endpoint (/example) that retrieves and processes data using RxJS. Here's a breakdown of the code:

  • We import the necessary modules from @nestjs/common and rxjs.
  • The ExampleController class is decorated with the @Controller decorator, specifying the base route path as 'example'.
  • The getData() method is decorated with @Get() to handle HTTP GET requests to the /example route.
  • Within getData(), we call the fetchData() method, which returns an Observable representing an asynchronous data retrieval. We simulate this retrieval by using a setTimeout function to emit a single value ('Example data') and complete the observable after 1 second.
  • The map operator is applied to the Observable returned by fetchData(). This operator transforms the emitted data by passing it to the processData() method, which converts it to uppercase.
  • The transformed data is returned as the response of the /example endpoint.

This example demonstrates how you can use RxJS to handle asynchronous operations and apply transformations to the data stream within a NestJS controller. You can build upon this foundation to implement more complex asynchronous workflows and handle data dependencies using RxJS operators and observables.

Here's a continuation of the example code, focusing on consuming the Observable in a NestJS service:

import { Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { HttpClient } from '@nestjs/common';

@Injectable()
export class ExampleService {
  constructor(private readonly http: HttpClient) {}

  fetchData(): Observable<any> {
    const apiUrl = 'https://api.example.com/data';
    return this.http.get(apiUrl);
  }

  processData(data: any): any {
    // Perform any processing on the data
    return {
      ...data,
      processed: true,
    };
  }
}

In this example, we have a NestJS service (ExampleService) that is responsible for fetching and processing data using RxJS Observables. Here's an explanation of the code:

  • We import the necessary modules from @nestjs/common and rxjs, including the HttpClient module for making HTTP requests.
  • The ExampleService class is decorated with the @Injectable() decorator, indicating that it can be injected as a dependency.
  • We inject the HttpClient into the service's constructor using dependency injection.
  • The fetchData() method uses the http.get() method from the HttpClient to make an HTTP GET request to an API endpoint (apiUrl). The method returns an Observable that represents the asynchronous data retrieval.
  • The processData() method takes the retrieved data and performs any necessary processing on it.
  • You can add additional methods to the service to handle further data operations using RxJS operators and observables.

By separating the data retrieval and processing logic into a service, you can leverage the power of RxJS Observables within your NestJS application. This allows you to handle asynchronous operations, make API requests, process data, and respond to events in a reactive and composable manner.

Remember to import the required modules and configure the HttpClient module properly in your NestJS application's module file (app.module.ts) to enable the use of the HttpClient within services.

By utilizing RxJS Observables in your NestJS service, you can create a robust and reactive data pipeline that seamlessly integrates with external APIs, performs data transformations, and enables efficient handling of asynchronous operations.

Another example

Here's an example of how you can create a NestJS API for a Todo application using RxJS Observables:

import { Controller, Get, Post, Body, Param, Delete } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

interface Todo {
  id: string;
  title: string;
  completed: boolean;
}

@Controller('todos')
export class TodoController {
  private todos: Todo[] = [];

  @Get()
  getAllTodos(): Observable<Todo[]> {
    return new Observable(observer => {
      observer.next(this.todos);
      observer.complete();
    });
  }

  @Post()
  createTodo(@Body() todo: Todo): Observable<Todo> {
    return new Observable(observer => {
      this.todos.push(todo);
      observer.next(todo);
      observer.complete();
    });
  }

  @Delete(':id')
  deleteTodoById(@Param('id') id: string): Observable<void> {
    return new Observable(observer => {
      const index = this.todos.findIndex(todo => todo.id === id);
      if (index !== -1) {
        this.todos.splice(index, 1);
      }
      observer.next();
      observer.complete();
    });
  }

  @Post(':id/complete')
  completeTodoById(@Param('id') id: string): Observable<Todo> {
    return new Observable(observer => {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) {
        todo.completed = true;
        observer.next(todo);
      }
      observer.complete();
    });
  }
}

In this example, we have a TodoController that handles CRUD operations for a Todo application. Here's an explanation of the code:

  • We import the necessary modules from @nestjs/common and rxjs.
  • The TodoController class is decorated with the @Controller decorator, specifying the base route path as 'todos'.
  • The getAllTodos() method handles the GET request to /todos and returns an observable that emits the list of todos.
  • The createTodo() method handles the POST request to /todos and returns an observable that emits the created todo.
  • The deleteTodoById() method handles the DELETE request to /todos/:id and returns an observable that emits no value upon successful deletion.
  • The completeTodoById() method handles the POST request to /todos/:id/complete and returns an observable that emits the updated todo upon successful completion.

In this example, we use simple in-memory storage (this.todos) to store the todo items. However, you can replace this with a database or any other persistent storage mechanism.

By returning RxJS Observables from the controller methods, you can leverage the power of reactive programming to handle asynchronous operations, perform data transformations, and compose multiple operations using operators such as map, filter, or switchMap.

Remember to import the required modules and configure the routing appropriately in your NestJS application's module file (app.module.ts) to enable the use of the TodoController.

With this example, you have a foundation for building a Todo API in NestJS using RxJS Observables. You can extend and customize this code to fit the specific needs of your application, including additional CRUD operations, validation, authentication, and more.