đź“– Guide Documents
Learn from Examples
NestJS WebSocket

Outline

If you develop websocket application, I recommend integrate TGrid with NestJS / Nestia.

It's because you can manage WebSocket API endpoints much effectively and easily by NestJS controller patterns. Also, you can make your server to support both HTTP and WebSocket protocols at the same time. NestJS controllers are compatible with both HTTP and WebSocket operations.

Furthermore, you can generate SDK (Software Development Kit) library for your client application through Nestia. With the automatically generated SDK, client developers no more need to write the WebSocket connection and RPC (Remote Procedure Call) codes manually, so that the client development becomes much easier and safer.

đź’»

Demonstration

You can run the example program on Playground Website (opens in a new tab), or local machine.

Terminal
git clone https://github.com/samchon/tgrid.example.nestjs
npm install
npm start

Server Program

Bootstrap

examples/nestjs/src/bootstrap.ts
import { WebSocketAdaptor } from "@nestia/core";
import { INestApplication } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
 
import { CalculateModule } from "./calculate.module";
 
export const bootstrap = async (): Promise<INestApplication> => {
  const app: INestApplication = await NestFactory.create(CalculateModule);
  await WebSocketAdaptor.upgrade(app);
  await app.listen(37_000, "0.0.0.0");
  return app;
};

To integrate TGrid with NestJS, you have to upgrade the NestJS application like above.

Just call the WebSocketAdaptor.upgrade() (opens in a new tab), then you can utilize TGrid in the NestJS server.

Controller

examples/nestjs/src/calculate.controller.ts
import { TypedRoute, WebSocketRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { Driver, WebSocketAcceptor } from "tgrid";
 
import { ICalcConfig } from "./api/interfaces/ICalcConfig";
import { ICalcEventListener } from "./api/interfaces/ICalcEventListener";
import { ICompositeCalculator } from "./api/interfaces/ICompositeCalculator";
import { IScientificCalculator } from "./api/interfaces/IScientificCalculator";
import { ISimpleCalculator } from "./api/interfaces/ISimpleCalculator";
import { IStatisticsCalculator } from "./api/interfaces/IStatisticsCalculator";
 
import { CompositeCalculator } from "./providers/CompositeCalculator";
import { ScientificCalculator } from "./providers/ScientificCalculator";
import { SimpleCalculator } from "./providers/SimpleCalculator";
import { StatisticsCalculator } from "./providers/StatisticsCalculator";
 
@Controller("calculate")
export class CalculateController {
  /**
   * Health check API (HTTP GET).
   */
  @TypedRoute.Get("health")
  public health(): string {
    return "Health check OK";
  }
 
  /**
   * Prepare a composite calculator.
   */
  @WebSocketRoute("composite")
  public async composite(
    @WebSocketRoute.Acceptor()
    acceptor: WebSocketAcceptor<
      ICalcConfig,
      ICompositeCalculator,
      ICalcEventListener
    >,
    @WebSocketRoute.Header() header: ICalcConfig,
    @WebSocketRoute.Driver() listener: Driver<ICalcEventListener>
  ): Promise<void> {
    const provider: CompositeCalculator = new CompositeCalculator(
      header,
      listener
    );
    await acceptor.accept(provider);
  }
 
  /**
   * Prepare a simple calculator.
   */
  @WebSocketRoute("simple")
  public async simple(
    @WebSocketRoute.Acceptor()
    acceptor: WebSocketAcceptor<
      ICalcConfig, // header
      ISimpleCalculator, // provider for remote client
      ICalcEventListener // provider from remote client
    >
  ): Promise<void> {
    const header: ICalcConfig = acceptor.header;
    const listener: Driver<ICalcEventListener> = acceptor.getDriver();
    const provider: SimpleCalculator = new SimpleCalculator(header, listener);
    await acceptor.accept(provider);
  }
 
  /**
   * Prepare a scientific calculator.
   */
  @WebSocketRoute("scientific")
  public async scientific(
    @WebSocketRoute.Acceptor()
    acceptor: WebSocketAcceptor<
      ICalcConfig,
      IScientificCalculator,
      ICalcEventListener
    >
  ): Promise<void> {
    const header: ICalcConfig = acceptor.header;
    const listener: Driver<ICalcEventListener> = acceptor.getDriver();
    const provider: ScientificCalculator = new ScientificCalculator(
      header,
      listener
    );
    await acceptor.accept(provider);
  }
 
  /**
   * Prepare a statistics calculator.
   */
  @WebSocketRoute("statistics")
  public async statistics(
    @WebSocketRoute.Acceptor()
    acceptor: WebSocketAcceptor<
      ICalcConfig,
      IStatisticsCalculator,
      ICalcEventListener
    >
  ): Promise<void> {
    const header: ICalcConfig = acceptor.header;
    const listener: Driver<ICalcEventListener> = acceptor.getDriver();
    const provider: IStatisticsCalculator = new StatisticsCalculator(
      header,
      listener
    );
    await acceptor.accept(provider);
  }
}

As you can see from the above code, CalculateController has many API operations, including both HTTP and WebSocket protocols. The CalculatorController.health() is an HTTP Get method operation, and the others are all WebSocket operations.

When defining WebSocket operation, attach @WebSocketRoute() decorator to the target controller method with path specification. Also, the controller method must have the @WebSocketRoute.Acceptor() decorated parameter with WebSocketAcceptor type, because you have to determine whether to WebSocketAcceptor.accept() the client's connection or WebSocketAcceptor.reject() it.

With such controller patterned WebSocket operation, you can manage WebSocket API endpoints much effectively and easily. Also, you can generate SDK (Software Development Kit) library for your client application through Nestia. Let's see how to generate SDK library, and how it would be looked like in the next section.

Software Development Kit

examples/nestjs/src/api/functional/calculate.ts
/**
 * @packageDocumentation
 * @module api.functional.calculate
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
//================================================================
import type { IConnection, Primitive } from "@nestia/fetcher";
import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher";
import { WebSocketConnector } from "tgrid";
import type { Driver } from "tgrid";
 
import type { ICalcConfig } from "../../interfaces/ICalcConfig";
import type { ICalcEventListener } from "../../interfaces/ICalcEventListener";
import type { ICompositeCalculator } from "../../interfaces/ICompositeCalculator";
import type { IScientificCalculator } from "../../interfaces/IScientificCalculator";
import type { ISimpleCalculator } from "../../interfaces/ISimpleCalculator";
import type { IStatisticsCalculator } from "../../interfaces/IStatisticsCalculator";
 
/**
 * Health check API (HTTP GET).
 *
 * @controller CalculateController.health
 * @path GET /calculate/health
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export async function health(connection: IConnection): Promise<health.Output> {
  return PlainFetcher.fetch(connection, {
    ...health.METADATA,
    path: health.path(),
  });
}
export namespace health {
  export type Output = Primitive<string>;
 
  export const METADATA = {
    method: "GET",
    path: "/calculate/health",
    request: null,
    response: {
      type: "application/json",
      encrypted: false,
    },
    status: null,
  } as const;
 
  export const path = () => "/calculate/health";
}
 
/**
 * Prepare a composite calculator.
 *
 * @controller CalculateController.composite
 * @path /calculate/composite
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export async function composite(
  connection: IConnection<composite.Header>,
  provider: composite.Provider,
): Promise<composite.Output> {
  const connector: WebSocketConnector<
    composite.Header,
    composite.Provider,
    composite.Listener
  > = new WebSocketConnector(connection.headers ?? ({} as any), provider);
  await connector.connect(
    `${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${composite.path()}`,
  );
  const driver: Driver<composite.Listener> = connector.getDriver();
  return {
    connector,
    driver,
  };
}
export namespace composite {
  export type Output = {
    connector: WebSocketConnector<Header, Provider, Listener>;
    driver: Driver<Listener>;
  };
  export type Header = ICalcConfig;
  export type Provider = ICalcEventListener;
  export type Listener = ICompositeCalculator;
 
  export const path = () => "/calculate/composite";
}
 
/**
 * Prepare a simple calculator.
 *
 * @controller CalculateController.simple
 * @path /calculate/simple
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export async function simple(
  connection: IConnection<simple.Header>,
  provider: simple.Provider,
): Promise<simple.Output> {
  const connector: WebSocketConnector<
    simple.Header,
    simple.Provider,
    simple.Listener
  > = new WebSocketConnector(connection.headers ?? ({} as any), provider);
  await connector.connect(
    `${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${simple.path()}`,
  );
  const driver: Driver<simple.Listener> = connector.getDriver();
  return {
    connector,
    driver,
  };
}
export namespace simple {
  export type Output = {
    connector: WebSocketConnector<Header, Provider, Listener>;
    driver: Driver<Listener>;
  };
  export type Header = ICalcConfig;
  export type Provider = ICalcEventListener;
  export type Listener = ISimpleCalculator;
 
  export const path = () => "/calculate/simple";
}
 
/**
 * Prepare a scientific calculator.
 *
 * @controller CalculateController.scientific
 * @path /calculate/scientific
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export async function scientific(
  connection: IConnection<scientific.Header>,
  provider: scientific.Provider,
): Promise<scientific.Output> {
  const connector: WebSocketConnector<
    scientific.Header,
    scientific.Provider,
    scientific.Listener
  > = new WebSocketConnector(connection.headers ?? ({} as any), provider);
  await connector.connect(
    `${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${scientific.path()}`,
  );
  const driver: Driver<scientific.Listener> = connector.getDriver();
  return {
    connector,
    driver,
  };
}
export namespace scientific {
  export type Output = {
    connector: WebSocketConnector<Header, Provider, Listener>;
    driver: Driver<Listener>;
  };
  export type Header = ICalcConfig;
  export type Provider = ICalcEventListener;
  export type Listener = IScientificCalculator;
 
  export const path = () => "/calculate/scientific";
}
 
/**
 * Prepare a statistics calculator.
 *
 * @controller CalculateController.statistics
 * @path /calculate/statistics
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export async function statistics(
  connection: IConnection<statistics.Header>,
  provider: statistics.Provider,
): Promise<statistics.Output> {
  const connector: WebSocketConnector<
    statistics.Header,
    statistics.Provider,
    statistics.Listener
  > = new WebSocketConnector(connection.headers ?? ({} as any), provider);
  await connector.connect(
    `${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${statistics.path()}`,
  );
  const driver: Driver<statistics.Listener> = connector.getDriver();
  return {
    connector,
    driver,
  };
}
export namespace statistics {
  export type Output = {
    connector: WebSocketConnector<Header, Provider, Listener>;
    driver: Driver<Listener>;
  };
  export type Header = ICalcConfig;
  export type Provider = ICalcEventListener;
  export type Listener = IStatisticsCalculator;
 
  export const path = () => "/calculate/statistics";
}
Terminal
npx nestia sdk

When you run npx nestia sdk command, SDK (Software Development Kit) library be generated.

Above file is one of the SDK library corresponding to the CalculateController class we've seen in the previous NestJS Controller section. Client developers can utilize the automatically generated SDK functions to connect to the WebSocket server, and interact it type safely. Also, HTTP operation is compatible with the WebSocket operation.

Let's see how client developer utilizes the SDK library in the next section.

Client Program

examples/nestjs/src/calculate.test.ts
import api from "./api";
import { ICalcEvent } from "./api/interfaces/ICalcEvent";
import { ICalcEventListener } from "./api/interfaces/ICalcEventListener";
 
export const testCalculateSdk = async () => {
  //----
  // HTTP PROTOCOL
  //---
  // CALL HEALTH CHECK API
  console.log(
    await api.functional.calculate.health({
      host: "http://127.0.0.1:37000",
    })
  );
 
  //----
  // WEBSOCKET PROTOCOL
  //---
  // PROVIDER FOR WEBSOCKET SERVER
  const stack: ICalcEvent[] = [];
  const listener: ICalcEventListener = {
    on: (evt: ICalcEvent) => stack.push(evt),
  };
 
  // DO CONNECT
  const { connector, driver } = await api.functional.calculate.composite(
    {
      host: "ws://127.0.0.1:37000",
      headers: {
        precision: 2,
      },
    },
    listener
  );
 
  // CALL FUNCTIONS OF REMOTE SERVER
  console.log(
    await driver.plus(10, 20), // returns 30
    await driver.multiplies(3, 4), // returns 12
    await driver.divides(5, 3), // returns 1.67
    await driver.scientific.sqrt(2), // returns 1.41
    await driver.statistics.mean(1, 3, 9) // returns 4.33
  );
 
  // TERMINATE
  await connector.close();
  console.log(stack);
};
Terminal
$ npm start
 
[Nest] 4328  - 05/15/2024, 3:19:50 AM     LOG [NestFactory] Starting Nest application...
[Nest] 4328  - 05/15/2024, 3:19:50 AM     LOG [InstanceLoader] CalculateModule dependencies initialized +5ms
[Nest] 4328  - 05/15/2024, 3:19:50 AM     LOG [RoutesResolver] CalculateController {/calculate}: +5ms
[Nest] 4328  - 05/15/2024, 3:19:50 AM     LOG [NestApplication] Nest application successfully started +2ms
 
Health check OK
30 12 1.67 1.41 4.33
[
  { type: 'plus', input: [ 10, 20 ], output: 30 },
  { type: 'multiplies', input: [ 3, 4 ], output: 12 },
  { type: 'divides', input: [ 5, 3 ], output: 1.67 },
  { type: 'sqrt', input: [ 2 ], output: 1.41 },
  { type: 'mean', input: [ 1, 3, 9 ], output: 4.33 }
]

Do import the SDK, and enjoy the type-safe and easy-to-use RPC (Remote Procedure Call).

Looking at the above code, the client application is calling a function of the automatically generated SDK (Software Development Kit) library, so that connecting to the websocket server, and starting interaction through RPC (Remote Procedure Call) concept with Driver<ICompositeCalculator> instance.

Doesn't the "SDK based development" seems much easier and safer than native websocket classes case? This is the reason why I've recommended to combine with the NestJS when using websocket protocol based network system.

This is the integration of TGrid with NestJS.

Next Chapter

We've learned how to utilize TGrid with many examples.

By the way, don't you want to know how to utilize TGrid in the real project?

In the next chapter, we'll see how TGrid be utilized in the real world.