
Nestjs application launch modes
Hello there!
My name is Yaroslav, and I am the chief dreamer at EasyLayer. Today, I want to share the results of a small investigate that will help answer an important question: What are the alternatives to launching a server in NestJS, beyond the traditional HTTP server?
This article is not a complete guide to NestJS and microservices, but it does explain the different ways to start NestJS applications. This investigate started at EasyLayer when we needed to run applications in different contexts. I will start with the simple and move to the complex.
Table of Contents:
- HTTP Server Mode
- Context Mode
- Special Case - Custom Web Server
- WebSocket Mode
- Listeners or Microservice Modes
- Special Case - WebSocket Gateway without other Listeners
- Special Case - Minimal Application for Sending Messages via Transport
HTTP Server Mode
In the world of NestJS, everything begins with the HTTP server, the most familiar component. It is the main way web applications interact with the outside world. With NestJS, we can easily create an efficient and well-structured HTTP server for most web applications.
To set everything up, we need to install a few important dependencies. Here is the basic package set for a Nest application:
yarn add @nestjs/core @nestjs/common @nestjs/platform-express rxjs reflect-metadata
I should mention that I have an excellent repository that serves as a sandbox where I've already set up everything for testing and exploring this module, so go ahead and pull it right away.
Many know that NestJS typically runs on Express, but it’s not the only option. NestJS is flexible enough to work with other platforms, such as Fastify. However, we'll focus on Express for starters.
Here’s a small TypeScript example of how to use app.listen() to start our server and handle requests. It might look like this:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();
Requests to this server are handled in controllers. Controllers in NestJS are the main entry points of the application and handle both HTTP requests and transport messages (more later).
Here’s a simple example of a controller that processes HTTP requests and responses. First, we need to create the controller itself. In NestJS, this is done using the @Controller() decorator, which marks a class as a controller:
import { Controller, Get } from '@nestjs/common'; @Controller('users') export class UsersController { @Get() findAll(): string { return 'This action returns all users'; } }
To ensure NestJS recognizes our controller, it must be registered in the appropriate module:
import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; @Module({ controllers: [UsersController] }) export class AppModule {}
The HTTP server mode in NestJS is the primary and most commonly used method for launching web applications. It is the core around which most NestJS applications are built.
However, it's important to remember that NestJS is not just a framework for creating HTTP servers. It offers much more in terms of capabilities and flexibility in the world of web development.
Context Mode
Now let's move on to one of NestJS's unique features — the context mode. This mode lets us use all the framework's features and capabilities without needing to run a full HTTP server. This opens up many applications, from console applications and server scripts to task schedulers and integration with queue systems.
First, we need to ensure all necessary NestJS dependencies are installed. If you followed the previous section, you should already have them:
yarn add @nestjs/core @nestjs/common reflect-metadata rxjs
Let's see how to create a context application. Here is an example code that shows this process:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { AppService } from './app.service'; async function bootstrap() { const app = await NestFactory.createApplicationContext(AppModule); const appService = appContext.get(AppService); await appService.doSomething(); } bootstrap();
In this example, we use NestFactory.createApplicationContext() instead of NestFactory.create(). This allows us to initialize a NestJS application in context mode, giving access to all NestJS services and modules without starting a web server.
Using context mode offers several benefits:
- You can use NestJS's powerful dependency injection and modular architecture in various scenarios, not just web applications.
- It is ideal for running database migrations and other maintenance tasks.
- Context mode is great for performing background tasks, integrating with queue systems, or creating CLI tools.
- It simplifies testing modules and services since you can run and test them in an isolated environment.
Therefore, using context mode lets you fully utilize NestJS's capabilities and architecture outside of web applications, making the framework a flexible and powerful tool for various development tasks.
Special Case - Custom Web Server
While it might seem unnecessary, you can run a contextual application along with a custom server in a provider. This setup allows for enhanced customization and integration with existing systems or special server configurations that are not supported by default in NestJS.
Here's how to create a context application that doesn't launch the standard NestJS HTTP server but allows the use of all its features:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.createApplicationContext(AppModule); } bootstrap();
In the AppModule, create a provider that uses Node.js http to create and launch an HTTP server.
import { Module } from '@nestjs/common'; import { createServer, IncomingMessage, ServerResponse } from 'http'; @Module({ providers: [ { provide: 'HTTP_SERVER', useFactory: () => { const server = createServer((req: IncomingMessage, res: ServerResponse) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello from custom HTTP server!'); }); server.listen(3000, () => { console.log('Custom HTTP server listening on port 3000'); }); return server; }, }, ], }) export class AppModule {}
In this example:
- We create an HTTP server using http.createServer().
- The server is launched on port 3000.
- Inside the server callback, we handle incoming HTTP requests.
It's important to note that if you create your own server within a NestJS provider, you may also need to manage its lifecycle (for example, closing the server when the application shuts down). This can be accomplished by implementing shutdown logic in the appropriate lifecycle hooks of the module or service.
This approach allows you to create and manage your own HTTP server within the NestJS ecosystem, leveraging the advantages of a context application. This method is suitable for scenarios where you need full control over HTTP request handling or when the standard features of NestJS do not meet your requirements.
WebSocket Mode
In NestJS, when working with WebSockets, we use special gateways instead of controllers. These gateways are powerful tools for handling two-way real-time interactions.
A gateway in NestJS is a class marked with the @WebSocketGateway() decorator. This decorator turns a regular class into a gateway that can handle WebSocket events.
One key feature of gateways in NestJS is their independence from any specific WebSocket platform. This means they can work with various WebSocket libraries, giving you flexibility in choosing the technology. Sometimes, this requires creating an appropriate adapter.
By default, NestJS supports two main WebSocket platforms: socket.io and ws. The choice between them depends on your specific requirements and preferences for the project.
To work with WebSockets in NestJS, you need to install the corresponding dependencies:
yarn add @nestjs/core @nestjs/common @nestjs/websockets @nestjs/platform-socket.io reflect-metadata rxjs
Let's look at an example of creating a WebSocket gateway in NestJS. We will make a gateway that runs alongside our main HTTP server but on a different port. We already have an HTTP server, and here is the gateway:
import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets'; @WebSocketGateway(3001) export class EventsGateway { @SubscribeMessage('message') handleEvent(@MessageBody() data: string): void { console.log('handle event'); } }
It's important to note that if we do not explicitly specify a port for the WebSocket gateway, it will operate on the same port as the HTTP server. This is standard behavior that suits most use cases.
You can send and receive messages through sockets using the decorators @SubscribeMessage() and @MessageBody().
The gateway is set up as a provider:
import { Module } from '@nestjs/common'; import { EventsGateway } from './events-gateway'; @Module({ imports: [], controllers: [], providers: [EventsGateway] }) export class AppModule {}
In NestJS, a WebSocket gateway creates its own listener, but it needs an active server to work properly. This means if you want to use WebSocket functionality, your app must also run an HTTP server. However, there's an exception to this rule. In microservice mode, a WebSocket gateway in NestJS can run without an active HTTP server. This makes microservice mode ideal for handling only WebSocket connections without needing a full web server. I will discuss this in the next section.
Listeners or Microservice Modes
In NestJS, a microservice is a standalone application that communicates with other parts of the system using an event-driven approach through various transports like TCP, MQTT, Redis, NATS, RabbitMQ, Kafka, and others.
To work with microservices in NestJS, you will need the core package @nestjs/microservices. This package provides the basic functionality for creating and managing microservices.
Depending on the chosen transport, you might need to install additional packages. For example: @nestjs/microservices already includes support for TCP and Redis. For working with Kafka, you might need kafkajs. For RabbitMQ, you should select an appropriate package that provides integration with this message queue system, such as amqplib.
Let's look at a simple example that illustrates how to turn an application into a microservice in NestJS:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Transport } from '@nestjs/microservices'; async function bootstrap() { const app = await NestFactory.createMicroservice(AppModule, { transport: Transport.TCP, options: { host: 'localhost', port: 3000, }, }); await app.listen(); } bootstrap();
In this example, we initialize our application as a microservice with TCP transport, listening on port 3000.
Instead of calling app.listen(), we can use app.init(); - in this case, the listener will not be started, but all modules will be initialized. This might be necessary in situations such as testing.
NestFactory.createMicroservice() is used to create and launch a microservice as an independent application. This method creates a separate application context, meaning that microservices created this way operate independently of HTTP servers or other microservices.
What does this mean? Let's consider a specific case:
const microserviceTcp = await NestFactory.createMicroservice(MicroserviceTCPModule, { transport: Transport.TCP, options: { host: 'localhost', port: 3001 }, }); const microserviceMqtt = await NestFactory.createMicroservice(MicroserviceMQTTModule, { transport: Transport.MQTT, options: { host: 'localhost', port: 3002 }, }); await microserviceTcp.listen(); await microserviceMqtt.listen();
In this example, we call NestFactory.createMicroservice() twice, with both microservices running in the same Node.js process. Each has its own configuration, services, controllers, etc. Since the microservices run in the same process, they share system resources such as memory and CPU.
This is not the best way to run multiple listeners; in reality, you might just need to run multiple listeners within an existing application, such as alongside the same HTTP server. Let's see how this can be done:
import { NestFactory } from '@nestjs/core'; import { Transport } from '@nestjs/microservices'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.connectMicroservice({ transport: Transport.TCP, options: { host: 'localhost', port: 3001, }, }); app.connectMicroservice({ transport: Transport.RMQ, options: { urls: ['amqp://localhost'], queue: 'nest_queue', queueOptions: { durable: false, }, }, }); await app.startAllMicroservices(); await app.listen(3000); } bootstrap();
Here, we are running an HTTP server on port 3000 while simultaneously creating a TCP microservice on port 3001. Additionally, we configure a RabbitMQ microservice that listens to the "nest_queue".
Important Additional Dependencies: for working with specific transports such as RabbitMQ, additional packages are required. In our case, we use amqplib for integration with RabbitMQ.
app.connectMicroservice() is used to register the microservice within the context of the existing NestJS application. This allows us to manage microservices within the main application. As you can see, we can add as many listeners as we want.
After registering all microservices using app.connectMicroservice(), calling await app.startAllMicroservices() starts all connected microservices. This provides a more organized and centralized way to manage different parts of your NestJS application.
Let's explore how to respond to these messages. In NestJS architecture, controllers play a central role not only in handling HTTP requests but also in receiving messages from other microservices using various transports, except WebSockets. Controllers in NestJS are the main entry points of the application, which is crucial for organizing your code properly. When handling messages coming through any transport other than WebSockets, they must pass through controllers. Otherwise, if you try to handle these messages in providers, they will be ignored.
import { Controller } from '@nestjs/common'; import { Ctx, MessagePattern, EventPattern, Payload, RmqContext, TcpContext } from '@nestjs/microservices'; @Controller() export class AppController { @EventPattern('create_item') async handleCreateItem(@Payload() data: any, @Ctx() context: RmqContext) { console.log('Item creation:', data); } @MessagePattern('get_status') getStatus(@Payload() data: any, @Ctx() context: TcpContext) { console.log('Status request:', data); return 'OK'; } }
NestJS provides various decorators for handling incoming messages in microservices:
MessagePattern is used for handling requests that expect a response from the microservice. When a controller method is marked with the MessagePattern decorator, it is expected to return a response. This response is then sent back to the request sender.
Example: Used in request-response scenarios where the client sends a request and expects to receive some data in return.
EventPattern is used for handling event messages that usually do not need an immediate response from the microservice. Unlike MessagePattern, methods marked with EventPattern generally do not send a response back to the sender. Instead, they act as event handlers, performing specific actions in response to these events without sending back any data.
Example: Used in scenarios where it is necessary to react to certain events (such as data updates, notifications) without needing to send a response message.
Special Case - WebSocket Gateway without other Listeners
To begin, initialize a TCP microservice:
import { NestFactory } from '@nestjs/core'; import { Transport, MicroserviceOptions } from '@nestjs/microservices'; import { AppModule } from './app.module'; async function bootstrap() { const tcpApp = await NestFactory.createMicroservice(AppModule, { transport: Transport.TCP, options: { host: 'localhost', port: 3000, }, }); await tcpApp.init(); } bootstrap();
In this example, we create a TCP microservice but only call app.init() to avoid starting the TCP listener. This initializes all modules and services.
Now, let's configure and launch a WebSocket gateway on port 3001:
import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets'; @WebSocketGateway(3001) export class EventsGateway { @SubscribeMessage('message') handleEvent(@MessageBody() data: string): void { console.log('handle event'); } } import { Module } from '@nestjs/common'; import { EventsGateway } from './events-gateway'; @Module({ providers: [EventsGateway] }) export class AppModule {}
Let's look at an example of how to send messages to a microservice using the RabbitMQ transport.
To send messages in NestJS using RabbitMQ, you can use the client registration mechanism provided by the framework. This mechanism allows you to interact with other microservices by sending messages through specific transports, in this case, RabbitMQ.
Make sure you have the necessary dependencies installed:
yarn add amqplib @nestjs/microservices
We create and configure a ClientProxy in our application module. After registering the client in the module, you can inject it into a service to send messages.
import { Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { AppService } from './app.service'; @Module({ imports: [ ClientsModule.register([ { name: 'RABBITMQ_SERVICE', transport: Transport.RMQ, options: { urls: ['amqp://localhost'], queue: 'nest_queue', queueOptions: { durable: false }, }, }, ]), ], providers: [AppService], }) export class AppModule {}
Here we register the RabbitMQ client using ClientsModule.register. We specify the client name RABBITMQ_PRODUCER, the transport (RabbitMQ), and the connection options.
import { Injectable, Inject } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; @Injectable() export class AppService { constructor(@Inject('RABBITMQ_PRODUCER') private readonly client: ClientProxy) {} async sendMessage(message: string) { return this.client.emit('nest_queue', { text: message }); } }
In AppService, the RabbitMQ client is injected through the constructor using the @Inject decorator. We use the client name RABBITMQ_PRODUCER that was defined when registering the client in the module.
Special Case - Minimal Application for Sending Messages via Transport
To implement a minimal NestJS application capable of sending messages through a specific transport, you can use a context application (NestApplicationContext). This allows you to create a lightweight application without the overhead of an HTTP server, making it ideal for simple tasks like sending messages.
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { AppService } from './app.service'; async function bootstrap() { try { const appContext = await NestFactory.createApplicationContext(AppModule) const appService = appContext.get(AppService); await appService.sendMessage('message from context event provider'); } catch (error) { console.log('Application error: ', error); process.exit(1); } } bootstrap();
import { Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { AppService } from './app.service'; @Module({ imports: [ ClientsModule.register([ { name: 'RABBITMQ_PRODUCER', transport: Transport.RMQ, options: { urls: ['amqp://localhost'], queue: 'nest_queue', queueOptions: { durable: false }, }, }, ]), ], providers: [AppService], }) export class AppModule {}
import { Injectable, Inject } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; @Injectable() export class AppService { constructor(@Inject('RABBITMQ_PRODUCER') private readonly client: ClientProxy) {} async sendMessage(message: string) { return this.client.emit('create_item', { text: message }); } }
In this example:
- A context application is created, allowing us to work with the NestJS framework without needing to run a full-fledged HTTP server.
- A RabbitMQ client is initialized to send messages to the queue nest_queue.
- The client.emit method is used to send a message and wait for a response (if needed).
This approach provides an efficient solution for tasks related to microservice communication without needing to create full server applications.
References
GitHub repository - https://github.com/EasyLayer/nestjs-launch-modes-poc
Discussion - https://github.com/EasyLayer/core/discussions/1
Popular Articles
- Can Bitcoin be Hacked? What is Blockchain?07.04.2024
- Nestjs application launch modes14.11.2024
- What is cryptocurrency?23.01.2024
- Why I Should Invest in Cryptocurrency? Understanding Its Impact Potential19.01.2024
- What is Bitcoin?23.01.2024