WebSocket
Stratal provides first-class WebSocket support through the stratal/websocket module, built on top of Cloudflare Workers WebSocket API. You define WebSocket endpoints using a decorator-based gateway pattern that mirrors the controller pattern you already know.
Creating a gateway
Section titled “Creating a gateway”Use the @Gateway() decorator to mark a class as a WebSocket gateway. The decorator takes a route path, just like @Controller():
import { Gateway, OnMessage, OnClose, type GatewayContext } from 'stratal/websocket'
@Gateway('/ws/chat')class ChatGateway { @OnMessage() handleMessage(evt: MessageEvent, ctx: GatewayContext) { ctx.send(`echo:${evt.data as string}`) }
@OnClose() handleClose(evt: CloseEvent, ctx: GatewayContext) { console.log('Client disconnected') }}The @Gateway() decorator internally applies @Transient(), so each WebSocket connection gets a fresh instance resolved from the DI container. You can inject dependencies through the constructor just like any other provider.
Event handlers
Section titled “Event handlers”Decorate methods with event decorators to handle WebSocket lifecycle events:
| Decorator | Event signature | Description |
|---|---|---|
@OnMessage() | (evt: MessageEvent, ctx: GatewayContext) | Called when a message is received |
@OnClose() | (evt: CloseEvent, ctx: GatewayContext) | Called when the connection closes |
@OnError() | (evt: Event, ctx: GatewayContext) | Called when a WebSocket error occurs |
Each gateway can have at most one handler per event type. All three are optional — define only the ones you need.
@Gateway('/ws/chat')class ChatGateway { @OnMessage() handleMessage(evt: MessageEvent, ctx: GatewayContext) { const data = evt.data as string ctx.send(`ack:${data}`) }
@OnClose() handleClose(evt: CloseEvent, ctx: GatewayContext) { console.log(`Closed with code ${evt.code}`) }
@OnError() handleError(evt: Event, ctx: GatewayContext) { console.error('WebSocket error', evt) }}GatewayContext
Section titled “GatewayContext”GatewayContext extends RouterContext with WebSocket-specific methods. Your event handlers receive it as the second argument.
WebSocket methods
Section titled “WebSocket methods”| Method / Property | Type | Description |
|---|---|---|
send(data) | void | Send a string, ArrayBuffer, or Uint8Array to the client |
close(code?, reason?) | void | Close the WebSocket connection |
readyState | WSReadyState | Current connection state |
ws | WSContext | The underlying Hono WSContext for advanced use |
Inherited from RouterContext
Section titled “Inherited from RouterContext”| Method | Description |
|---|---|
header(name) | Read a header from the upgrade request |
param(name) | Read a route parameter |
query(name) | Read a query string parameter |
getContainer() | Access the DI container |
getLocale() | Get the current locale from the i18n context |
@Gateway('/ws/rooms/:roomId')class RoomGateway { @OnMessage() handleMessage(evt: MessageEvent, ctx: GatewayContext) { const roomId = ctx.param('roomId') const token = ctx.header('Authorization') ctx.send(`Room ${roomId} received: ${evt.data as string}`) }}Module registration
Section titled “Module registration”Gateways are registered in the controllers array of a module, alongside regular controllers. Stratal automatically detects which classes are gateways and registers them as WebSocket routes.
import { Module } from 'stratal/module'
@Module({ controllers: [ChatGateway, RoomGateway],})export class ChatModule {}Guards
Section titled “Guards”@UseGuards() works on gateways the same way it works on controllers. Guards execute during the HTTP upgrade request, before the WebSocket connection is established:
import { UseGuards } from 'stratal/router'import { AuthGuard } from './auth.guard'
@UseGuards(AuthGuard)@Gateway('/ws/chat')class ChatGateway { @OnMessage() handleMessage(evt: MessageEvent, ctx: GatewayContext) { ctx.send(`echo:${evt.data as string}`) }}If a guard rejects the request, the upgrade is denied and the WebSocket connection is never opened.
API versioning
Section titled “API versioning”Versioned paths work the same as controllers. Pass a version option in the second argument to @Gateway() to register the gateway under a version prefix:
@Gateway('/ws/chat', { version: '1' })class ChatGatewayV1 { // Registered at /v1/ws/chat}The version option accepts a string, string[] for multiple versions, or VERSION_NEUTRAL to opt out of versioning — identical to the @Controller() pattern. See API Versioning for full details.
Next steps
Section titled “Next steps”- WebSocket Testing for testing gateways with
module.ws(). - Guards to protect upgrade requests.
- Middleware for shared logic across controllers and gateways.
- Durable Objects for stateful WebSocket connections that persist across requests.