@WebSocketRoute
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:
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.
Controller
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:
- The client connects to
/calculate/startover WebSocket. - The server accepts the connection and exposes a
Calculatorinstance. - The client receives a
Driver<ICalculator>— every method on it returns aPromise<R>because the call hops the network. - Each operation also pushes an event back to the client via the
IListenerdriver.
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:
| Decorator | What 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> { ... }A route path follows the syntax of an HTTP route in NestJS 11 (Express 5’s path-to-regexp): :name parameters, *name wildcards, {...} optional groups, and a literal colon escaped as \\: or spelled ::, as @TypedParam describes.
Path / header / query are validated against their declared types — invalid handshakes are rejected before your handler runs.
Header parameters receive the successful assert return value or validate result’s data; is keeps the original input. Clone validators provide a copy while acceptor.header retains the original handshake header. Prune validators mutate that header in place.
Every failing request ends with a close code and a reason, so the client’s connect() or pending call rejects with a WebSocketError rather than waiting:
| What failed | Code | Reason |
|---|---|---|
| No route matches the path | 1002 | WebSocket API not found |
| A param, header, or query fails its type | 1003 | the failing property, like invalid type on $input.name, expect to be string |
Your handler throws before accept() | 1008 | the error’s message |
Your handler throws after accept() | 1011 | the error’s message |
A close reason holds at most 123 bytes of UTF-8, so a longer message is cut at a character boundary.
The acceptor lifecycle
WebSocketAcceptor<Header, Provider, Listener> is a state machine with three useful methods:
accept(provider)— agree to the handshake, exposeproviderto 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
- URI versions follow NestJS metadata. A method’s
@Version()overrides its controller’s version, and absent metadata uses the application’s default version. An explicit empty version array registers no route; it does not inherit another version.VERSION_NEUTRALomits the version segment. SDK and Swagger generation use the same precedence. - Acceptor parameter is required. Without an
@WebSocketRoute.Acceptor()parameter the build fails: the transform reports that the method must have at least one parameter decorated by@WebSocketRoute.Acceptor(). - Driver methods are async on the wire. A server-side
plus(x: number, y: number): numberbecomesplus(x, y): Promise<number>in the client’sDriver<T>. - One driver per direction. Server-to-client calls go through
Driver<Listener>; client-to-server calls go through theProviderinstance you pass toacceptor.accept(). They’re independent. - No SDK without TGrid. The client SDK pulls in
tgridas a runtime dependency. - The header is an object, or
undefinedfor none. The SDK sends the handshake header asconnection.headers, so theHeaderofWebSocketAcceptor<Header, Provider, Listener>and the type of an@WebSocketRoute.Header()parameter must be an object type orundefined. TGrid itself accepts any header, butnestia sdkreports anull, nullable, or primitive one by the parameter’s name rather than generating a client that does not compile.
References
- TGrid Docs — RPC concepts.
- TGrid + NestJS example — the calculator pattern in full.
- Playground — same example, runs in the browser.
See also
- TypedRoute — HTTP-side route decorators.
- SDK — how WebSocket routes appear in the generated client.