How to pass a dynamic port to the Websockets-gateway in NestJS?

Viewed 1124

I wanted to dynamically set the Websockets-gateway port from config in NestJS. Below is my websockets-gateway code.

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

const WS_PORT = parseInt(process.env.WS_PORT);

@WebSocketGateway(WS_PORT)
export class WsGateway {
  constructor() {
    console.log(WS_PORT);
  }
}

But the WS_PORT is always NaN.

This is my bootstrap function insdie main.ts :

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: false });
  const configService = app.get(ConfigService);
  initAdapters(app);
  await app.listen(configService.get(HTTP_PORT), () => {
    console.log('Listening on port ' + configService.get(HTTP_PORT));
  });
}

Below is my app.module.ts :

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: './src/config/dev.env',
      isGlobal: true,
    }),
    RedisModule,
    SocketStateModule,
    RedisPropagatorModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>(JWT_SECRET_KEY),
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AppController],
  providers: [WsGateway, AppService],
})
export class AppModule {}

I put a console log in the Gateway constructor to print the value of 'WS_PORT' but it's always NaN.

[Nest] 13252  - 10/04/2021, 5:05:34 PM     LOG [NestFactory] Starting Nest application...
NaN

Thanks in advance.

3 Answers

I could not find a way to add dynamic data to the decorator. So to be able to dynamically choose the port and other configurations I had to:

  • Create an adapter for socket-io:
  • Tell NestJs to use the new adapter

SocketIoAdapter.ts

import { INestApplicationContext } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { ConfigService } from '@nestjs/config';

export class SocketIoAdapter extends IoAdapter {
constructor(
  private app: INestApplicationContext,
  private configService: ConfigService,
) {
  super(app);
}

createIOServer(port: number, options?: ServerOptions) {
  port = this.configService.get<number>('SOCKETIO.SERVER.PORT');
  const path = this.configService.get<string>('SOCKETIO.SERVER.PATH');
  const origins = this.configService.get<string>(
    'SOCKETIO.SERVER.CORS.ORIGIN',
  );
  const origin = origins.split(',');
  options.path = path;
  options.cors = { origin };
  const server = super.createIOServer(port, options);
  return server;
}
}

Now, you need to edit the main.ts to use the adapter

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { SocketIoAdapter } from './socket-io/socket-io.adapter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  const hosts = configService.get<string>('CORS.HOST');
  const hostsArray = hosts.split(',');
  app.enableCors({
    origin: hostsArray,
    credentials: true,
  });
//Here you use the adapter and sent the config service
  app.useWebSocketAdapter(new SocketIoAdapter(app, configService));
  await app.listen(4300);
}
bootstrap();


In this case I set the port and the cors origin, here an example of the conf file (using .env) env.local

SOCKETIO.SERVER.PORT=4101
SOCKETIO.SERVER.PATH=
SOCKETIO.SERVER.CORS.ORIGIN=http://localhost:4200,http://localhost.com:8080

Here a link to config service Config Service NestJs

port = this.configService.get<number>('SOCKETIO.SERVER.PORT');

In my case I found its return string(from .env), port gets 'string' but not 'number',

but if put parseInt(this.configService.get<number>('SOCKETIO.SERVER.PORT'), 10); then it is ok

Mind that socket-io ports must be the same on server & client side

You can do it relatively straightforward if you decorate the Gateway before app.init is called:

  1. Import the class in main.ts
  2. Get an instance of your ConfigurationService
  3. Manually call the decorator on the class with the config data
function decorateGateway(class_, config) {
  // Just calling the decorator as a function with the class
  // as argument does the same as `@WebSocketGateway`
  WebSocketGateway({
    cors: {
      origin: config.get("websocket.cors.origin"),
    }
  })(class_)
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {});
  const config = app.get(ConfigService);
  decorateGateway(ChatGateway, config);
  ...
  app.init();
}

The tricky part with a Gateway is that it starts up together with the server, and the decorator metadata needs to be applied to the class earlier than for other components. You can do this in main.ts before app.init.

Related