Nest, a TypeScript-based ode.js framework, is officially released! (2)

Keywords: node.js Redis socket JSON TypeScript

This article is a translation with the original address: https://kamilmysliwiec.com/nest-final-release-is-here-node-js-framework-built-top-of-typescript Author, @Kamil My liwiec

The address of the last article is: https://segmentfault.com/a/1190000009560532

Middlewares

Middleware is a function that is called before a routing handler. Middleware functions can access request and response objects, so they can be modified. They can also serve as a barrier, and if the middleware function does not call next(), requests will never be processed by the routing handler.

Let's build a virtual authorization middleware. We will use the X-Access-Token HTTP header to provide the user name (which is a strange idea, but not important).

import { UsersService } from './users.service';
import { HttpException } from '@nestjs/core';
import { Middleware, NestMiddleware } from '@nestjs/common';

@Middleware()
export class AuthMiddleware implements NestMiddleware {
    constructor(private usersService: UsersService) {}

    resolve() {
        return (req, res, next) => {
            const userName = req.headers["x-access-token"];
            const users = this.usersService.getUsers();

            const user = users.find((user) => user.name === userName);
            if (!user) {
                throw new HttpException('User not found.', 401);
            }
            req.user = user;
            next();
        }
    }
}

Some facts are as follows:

  • You should use the @Middleware() annotation to tell Nest that this class is a middleware.

  • You can use the Nest Middleware interface, which forces you to use the resolve() method.

  • Middleware (like components) can inject dependencies through its constructors (dependencies must be part of a module).

  • The middleware must have a resolve() method, which must return another function (higher-order function). Why? Because there are many third-party plug-ins ready to use express middleware, you can simply use it in Nest, thanks to this solution.

Well, we have middleware ready, but we don't use it anywhere. Let's set it up:

import { Module, MiddlewaresConsumer } from '@nestjs/common';

@Module({
    controllers: [ UsersController ],
    components: [ UsersService ],
    exports: [ UsersService ],
})
export class UsersModule {
    configure(consumer: MiddlewaresConsumer) {
        consumer.apply(AuthMiddleware).forRoutes(UsersController);
    }
}

As shown above, modules can have other methods, configure(). This method takes the MiddlewaresConsumer object as a parameter, which can help us configure the middleware.

This object has the apply() method, which receives innumerable middleware (this method uses extension operators, so it can pass multiple comma-separated classes). The apply() method returns the object in two ways:

  • forRoutes(): We use this method to separate controllers or objects (with path and method attributes) by commas.

  • with(): We use this method to pass custom parameters to the resolve() middleware method.

How does it work?

When you pass UsersController in the forRoutes method, Nest will set middleware for each route in the controller:

GET: users
GET: users/:id
POST: users

But you can also directly define which path middleware should be used, just like this:

consumer.apply(AuthMiddleware).forRoutes({
    path: '*', method: RequestMethod.ALL
});

Chaining

You can simply call the apply() chain.

consumer.apply(AuthMiddleware, PassportMidleware)
    .forRoutes(UsersController, OrdersController, ClientController);
    .apply(...)
    .forRoutes(...);

order

Middleware is called in the same order as arrays. The middleware configured in the sub-module will be invoked after the parent module is configured.

Gateways Real-time Applications

Nest has a special component called gateway. Gateway helps us create real-time network applications. They are some encapsulated socket.io functions that are adapted to the framework.

Gateway is a component, so it can inject dependencies through constructors. Gateway can also be injected into another component.

import { WebSocketGateway } from '@nestjs/websockets';

@WebSocketGateway()
export class UsersGateway {}

By default, the server runs on port 80 and uses the default namespace. We can easily change these settings:

@WebSocketGateway({ port: 81, namespace: 'users' })

Of course, the server runs only when Users Gateway is in the component array, so we have to put it there:

@Module({
    controllers: [ UsersController ],
    components: [ UsersService, UsersGateway ],
    exports: [ UsersService ],
})

By default, gateways have three useful events:

There are special interfaces, OnGateway Init, OnGateway Connection and OnGateway Disconnect to help us manage the life cycle.

What is a message?

In the gateway, we can simply subscribe to the messages sent out:

import { WebSocketGateway, SubscribeMessage } from '@nestjs/websockets';

@WebSocketGateway({ port: 81, namespace: 'users' })
export class UsersGateway {
    @SubscribeMessage('drop')
    handleDropMessage(sender, data) {
        // sender is a native socket.io client object
    }
}

The client receives the following:

import * as io from 'socket.io-client';
const socket = io('http://URL:PORT/');
socket.emit('drop', { msg: 'test' });

@WebSocketServer()

If you want to assign the selected socket.io local server instance properties, you can simply decorate with the @WebSocketServer() decorator.

import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';

@WebSocketGateway({ port: 81, namespace: 'users' })
export class UsersGateway {
    @WebSocketServer() server;

    @SubscribeMessage('drop')
    handleDropMessage(sender, data) {
        // sender is a native socket.io client object
    }
}

The server will be assigned values after initialization.

Gateway Middleware

Gateway middleware is almost the same as router middleware. Middleware is a function that is called before the gateway message user. Gateway middleware functions can access local socket objects. They are like barriers. If the middleware function does not call next(), the message will never be processed by the user.

For example:

@Middleware()
export class AuthMiddleware implements GatewayMiddleware {
    public resolve(): (socket, next) => void {
        return (socket, next) => {
            console.log('Authorization...');
            next();
        };
    }
}

Some facts about gateway middleware:

Well, we're ready for middleware, but we don't use it anywhere. Let's set it up:

@WebSocketGateway({
    port: 2000,
    middlewares: [ ChatMiddleware ],
})
export class ChatGateway {}

As shown above, @WebSocketGateway() accepts the additional tuple attribute - middlewares, which is a middleware array. These middleware will be invoked before the message handler.

Reactive Streams

Nest Gateway is a simple component that can be injected into another component. This feature makes it possible for us to choose how we will react to messages.

Of course, only if necessary can we inject components into the gateway and invoke its appropriate methods.

But there is another solution, Gateway Reactive Streams. You can be at Here Read more about them.

Microservices support

Converting Nest applications to Nest microservices is very simple.

Let's see how to create a Web application:

const app = NestFactory.create(ApplicationModule);
app.listen(3000, () => console.log('Application is listening on port 3000'));

Now switch to Microsoft:

const app = NestFactory.createMicroservice(ApplicationModule, { port: 3000 });
app.listen() => console.log('Microservice is listening on port 3000'));

This is it!

Communicate through TCP

By default, Nest microservices listen for messages through the TCP protocol. This means that @RequestMapping() and @Post(), @Get() and so on are not useful now because it maps HTTP requests. So how do micro services identify messages? Just by mode.

What is a pattern? Nothing special. It's an object, string or number (but that's not a good idea).

Let's create MathController:

import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class MathController {
    @MessagePattern({ cmd: 'add' })
    add(data, respond) {
        const numbers = data || [];
        respond(null, numbers.reduce((a, b) => a + b));
    }
}

You may see that if you want to create a message handler, you have to decorate it with @Message Pattern. In this example, I chose {cmd:'add'} as the pattern.

The handler method receives two parameters:

Client

You already know how to receive messages. Now let's see how to send them from another microserver or Web application.

Before you start, Nest must know the location of the mail you want to send. Simply, you just need to create an @Client object.

import { Controller } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';

@Controller()
export class ClientController {
    @Client({ transport: Transport.TCP, port: 5667 })
    client: ClientProxy;
}

@ Client() Decorator receives objects as parameters. This object can have three attributes:

Using Client

Let's create a custom path to test our communication.

import { Controller, Get } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';

@Controller()
export class ClientController {
    @Client({ transport: Transport.TCP, port: 5667 })
    client: ClientProxy;

    @Get('client')
    sendMessage(req, res) {
        const pattern = { command: 'add' };
        const data = [ 1, 2, 3, 4, 5 ];

        this.client.send(pattern, data)
            .catch((err) => Observable.empty())
            .subscribe((result) => res.status(200).json({ result }));
    }
}

As you can see, in order to send messages, you have to use the send method, which receives message patterns and data as parameters. This method follows Rxjs The package returns an Observable.

This is a very important feature because Observables provides a surprising set of operations to handle, such as combine, zip, retryWhen, timeout, and so on.

Of course, if you want to use Promise instead of Observables, you can use the toPromise() method directly.

That's it.

Now, when someone sends a / test request (GET), how should it be applied (if both microservices and web applications are available):

{
    "result": 15
}

Redis

There is another way to work with Nest microservices. We can use Redis's publish/subscribe functionality instead of direct TCP communication.

You must install Redis before using it.

Creating Redis Micro Services

To create a Redis microservice, you have to pass other configurations in the NestFactory.createMicroservice() method.

const app = NestFactory.createMicroservice(
    MicroserviceModule,
    {
        transport: Transport.REDIS,
        url: 'redis://localhost:6379'
    }
);
app.listen(() => console.log('Microservice listen on port:', 5667 ));

That's it. Now, your microservice will subscribe to messages posted through Redis. Other approaches remain the same, such as schema and error handling.

Redis client

Now let's see how to create a client. Previously, your client configuration was as follows:

@Client({ transport: Transport.TCP, port: 5667 })
client: ClientProxy;

We want to use Redis instead of TCP, so we have to change these configurations:

@Client({ transport: Transport.REDIS, url: 'redis://localhost:6379' })
client: ClientProxy;

It's easy, isn't it? This is it. Other functions are the same as those in TCP communication.

Sharing module

Nest module can export other components. This means that we can easily share component instances between them.

The best way to share instances between two or more modules is to create shared modules.

For example, if we want to share ChatGateway components throughout the application, we can do this:

import { Module, Shared } from '@nestjs/common';

@Shared()
@Module({
    components: [ ChatGateway ],
    exports: [ ChatGateway ]
})
export class SharedModule {}

Then, we just need to import this module into another module, which should share component instances:

@Module({
    modules: [ SharedModule ]
})
export class FeatureModule {}

That's it.

Note that the scope of shared modules can also be defined directly. Learn more details.

Dependency Injection

Dependency injection is a powerful mechanism that can help us manage our class dependencies easily. It is a very popular language, such as C# and Java.

In Node.js, these functions are not important because we already have a magical module loading system, such as the ease of sharing instances between files.

Module loading system is sufficient for small and medium-sized applications. As code grows, it becomes more difficult to smoothly organize dependencies between layers. One day it will explode.

It is more intuitive than the DI constructor. That's why Nest has its own DI system.

Custom Components

As you've learned, it's very easy to add components to the selected module.

@Module({
    controllers: [ UsersController ],
    components: [ UsersService ]
})

Yes, there are other cases where Nest allows you to use it.

Use value:

const value = {};
@Module({
    controllers: [ UsersController ],
    components: [
        { provide: UsersService, useValue: value }
    ],
})

When:

Use class:

@Component()
class CustomUsersService {}

@Module({
    controllers: [ UsersController ],
    components: [
        { provide: UsersService, useClass: CustomUsersService }
    ],
})

When:

Use factories

@Module({
    controllers: [ UsersController ],
    components: [
        ChatService,
        {
            provide: UsersService,
            useFactory: (chatService) => {
                return Observable.of('value');
            },
            inject: [ ChatService ]
        }
    ],
})

When:

Remember:

Customized providers

@Module({
    controllers: [ UsersController ],
    components: [
        { provide: 'isProductionMode', useValue: false }
    ],
})

When:

Please note that:

How do you use it?

Using the selected key to inject custom providers / values, you have to tell Nest, like this:

import { Component, Inject } from '@nestjs/common';

@Component()
class SampleComponent {
    constructor(@Inject('isProductionMode') isProductionMode: boolean) {
        console.log(isProductionMode); // false
    }
}

`

ModuleRef

Sometimes you may want to get component instances directly from module references. For Nest, it's not a big deal. You just need to inject ModuleRef into the class:

export class UsersController {
    constructor(
        private usersService: UsersService,
        private moduleRef: ModuleRef) {}
}

ModuleRef provides a method:

For example:

moduleRef.get<UsersService>(UsersService)

It returns an instance of the UsersService component from the current module.

test

Nest provides you with a set of testing tools that can be used to provide the testing process. There are two ways to test your components and controllers, isolate testing and use a dedicated Nest test tool.

Isolation test

Nest's controllers and components are simple JavaScript classes. This means that you can easily create them yourself:

const instance = new SimpleComponent();

If your class has other dependencies, you can use test doubles, for example Jasmine or Sinon Library:

const stub = sinon.createStubInstance(DependencyComponent);
const instance = new SimpleComponent(stub);

Special Nest Testing Tool

Another way to test application building blocks is to use a dedicated Nest test tool.

These test tools are placed in the static Test class (@nestjs/testing module).

import { Test } from '@nestjs/testing';

This class provides two approaches:

For example:

Test.createTestingModule({
    controllers: [ UsersController ],
    components: [ UsersService ]
});
const usersService = Test.get<UsersService>(UsersService);

`

Mocks, spies, stubs

Sometimes you may not want to use an instance of a component/controller. You can choose to use test doubles or customize values / objects.

const mock = {};
Test.createTestingModule({
    controllers: [ UsersController ],
    components: [
        { provide: UsersService, useValue: mock }
    ]
});
const usersService = Test.get<UsersService>(UsersService); // mock

Exception Filters

Using Nest, exception handling logic can be moved to a special class called Exception Filters.

How does it work?

Let's look at the following code:

@Get('/:id')
public async getUser(@Response() res, @Param('id') id) {
    const user = await this.usersService.getUser(id);
    res.status(HttpStatus.OK).json(user);
}

Imagine that the usersService.getUser(id) method throws a UserNotFoundException exception. We need to catch exceptions in the routing handler:

@Get('/:id')
public async getUser(@Response() res, @Param('id') id) {
    try {
        const user = await this.usersService.getUser(id);
        res.status(HttpStatus.OK).json(user);
    }
    catch(exception) {
        res.status(HttpStatus.NOT_FOUND).send();
    }
}

In summary, we must add try... catch blocks to every routing processing where exceptions may occur. Is there any other way? Yes, Exception Filters.

Let's create NotFoundException Filter:

import { Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';

export class UserNotFoundException {}
export class OrderNotFoundException {}

@Catch(UserNotFoundException, OrderNotFoundException)
export class NotFoundExceptionFilter implements ExceptionFilter {
    catch(exception, response) {
        response.status(HttpStatus.NOT_FOUND).send();
    }
}

`
Now, we just need to tell our Controller to use this filter:

import { ExceptionFilters } from '@nestjs/common';

@ExceptionFilters(NotFoundExceptionFilter)
export class UsersController {}

So if usersService.getUser(id) throws UserNotFoundException, the NotFoundException Filter will capture it.

More exception filters

Each controller may have an infinite number of exception filters (separated only by commas).

@ExceptionFilters(NotFoundExceptionFilter, AnotherExceptionFilter)

Dependency Injection

Exception filters are the same as components, so dependencies can be injected through constructors.

HttpException

Note: It is mainly used for REST applications.

Nest has an error handling layer that captures all unhandled exceptions.

If somewhere, in your application, you throw an exception that is not HttpException (or inherited), Nest will process it and return it to the following user's json object (500 status code):

{
    "message": "Unkown exception"
}

Exception hierarchy

In an application, you should create your own Exceptions Hierarchy. All HTTP exceptions should inherit from the built-in HTTP Exception.

For example, you can create NotFoundException and UserNotFoundException classes:

import { HttpException } from '@nestjs/core';

export class NotFoundException extends HttpException {
    constructor(msg: string | object) {
        super(msg, 404);
    }
}

export class UserNotFoundException extends NotFoundException {
    constructor() {
        super('User not found.');
    }
}

Then, if you throw UserNotFoundException somewhere in your application, Nest will respond to the user status code 404 and the following json objects:

{
    "message": "User not found."
}

It allows you to focus on logic and make your code easier to read.

Useful reference

Posted by wscreate on Thu, 27 Jun 2019 11:28:50 -0700