Skip to Content
πŸ“– Guide DocumentsCore LibraryWebSocketRoute

@WebSocketRoute

@nestia/core
export function WebSocketRoute(path?: string): MethodDecorator; export namespace WebSocketRoute { export function Acceptor(): ParameterDecorator; export function Driver(): ParameterDecorator; export function Header(): ParameterDecorator; export function Param(field: string): ParameterDecorator; export function Query(): ParameterDecorator; }

Type-safe WebSocket routes for NestJS. A @WebSocketRoute() method exposes an RPC endpoint that the client calls like a remote object β€” function calls in both directions, no message handlers, no string commands.

Built on TGridΒ , which handles the RPC layer. @WebSocketRoute is the NestJS adapter β€” it tells the framework how to mount the route and metadata for @nestia/sdk so the generated client gets typed driver.plus(4, 2) calls.

Vanilla NestJS @WebSocketGateway works fine for event-style WebSockets (chat messages, pub/sub). Reach for @WebSocketRoute when you want RPC β€” clients calling typed functions on the server (and the server calling typed functions on the client) with full IDE autocomplete on both sides.


Setup

Upgrade your NestJS application to accept WebSocket connections:

src/main.ts
import { WebSocketAdaptor } from "@nestia/core"; import { INestApplication } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; import { AppModule } from "./AppModule"; const main = async () => { const app: INestApplication = await NestFactory.create(AppModule); await WebSocketAdaptor.upgrade(app); await app.listen(3000); }; main().catch(console.error);

Without WebSocketAdaptor.upgrade(app), methods decorated with @WebSocketRoute() never receive connections.


Basic usage β€” a calculator

A typical pattern: client connects, server hands the client a typed provider, client calls methods on it, server pushes events back through a listener.

src/CalculateController.ts
import { WebSocketRoute } from "@nestia/core"; import { Controller } from "@nestjs/common"; import { Driver, WebSocketAcceptor } from "tgrid"; import { ICalculator } from "./api/structures/ICalculator"; import { IListener } from "./api/structures/IListener"; import { Calculator } from "./providers/Calculator"; @Controller("calculate") export class CalculateController { /** * Start a simple calculator session. */ @WebSocketRoute("start") public async start( @WebSocketRoute.Acceptor() acceptor: WebSocketAcceptor<any, ICalculator, IListener>, @WebSocketRoute.Driver() driver: Driver<IListener>, ): Promise<void> { await acceptor.accept(new Calculator(driver)); } }

What’s happening:

  1. The client connects to /calculate/start over WebSocket.
  2. The server accepts the connection and exposes a Calculator instance.
  3. The client receives a Driver<ICalculator> β€” every method on it returns a Promise<R> because the call hops the network.
  4. Each operation also pushes an event back to the client via the IListener driver.

The SDK function api.functional.calculate.start(...) is generated automatically when you run npx nestia sdk. The types stay aligned across the wire.


Parameter decorators

Inside a @WebSocketRoute(path) method, the same way you destructure HTTP requests, you can decorate WebSocket parameters:

DecoratorWhat it gives you
@WebSocketRoute.Acceptor()The WebSocketAcceptor<Header, Provider, Listener> β€” required to accept the connection.
@WebSocketRoute.Driver()The Driver<Listener> β€” a typed handle to the client’s provider.
@WebSocketRoute.Header()Typed header object the client sent at handshake.
@WebSocketRoute.Param("name")Path parameter (/calc/:id/advanced β†’ "id"). Same coercion as @TypedParam.
@WebSocketRoute.Query()Typed query-string DTO. Same parsing as @TypedQuery.
@WebSocketRoute(":id/advanced") public async advanced( @WebSocketRoute.Param("id") id: string & tags.Format<"uuid">, @WebSocketRoute.Header() header: Partial<IHeader> | undefined, @WebSocketRoute.Query() memo: IMemo, @WebSocketRoute.Acceptor() acceptor: WebSocketAcceptor<IHeader, IAdvCalculator, IListener>, @WebSocketRoute.Driver() driver: Driver<IListener>, ): Promise<void> { ... }

Path / header / query are validated against their declared types β€” invalid handshakes are rejected before your handler runs.


The acceptor lifecycle

WebSocketAcceptor<Header, Provider, Listener> is a state machine with three useful methods:

  • accept(provider) β€” agree to the handshake, expose provider to the client.
  • reject(status, reason) β€” refuse the handshake (auth failure, etc.).
  • close(code?, reason?) β€” terminate the connection after acceptance.

The convention: validate the handshake first, then accept. If anything is wrong, reject:

@WebSocketRoute("auth") public async auth( @WebSocketRoute.Header() header: { token?: string }, @WebSocketRoute.Acceptor() acceptor: WebSocketAcceptor<{}, ISession, ISessionListener>, ): Promise<void> { const session = await this.tokens.verify(header.token); if (!session) return acceptor.reject(1008, "Invalid token"); await acceptor.accept(this.sessions.create(session)); }

Caveats

  • Acceptor parameter is required. Without an @WebSocketRoute.Acceptor() parameter, the route never accepts the handshake and SDK generation skips it.
  • Driver methods are async on the wire. A server-side plus(x: number, y: number): number becomes plus(x, y): Promise<number> in the client’s Driver<T>.
  • One driver per direction. Server-to-client calls go through Driver<Listener>; client-to-server calls go through the Provider instance you pass to acceptor.accept(). They’re independent.
  • No SDK without TGrid. The client SDK pulls in tgrid as a runtime dependency.

References

See also

  • TypedRoute β€” HTTP-side route decorators.
  • SDK β€” how WebSocket routes appear in the generated client.
Last updated on