Skip to main content
Talos models WebSockets as ordinary routes. On the server, @talosjs/socket lets you declare a socket endpoint with the @Route.socket(path, config) decorator and implement a controller whose index(context) runs on connect, with a typed context.channel for subscribing, publishing, and sending. On the client, @talosjs/socket-client ships a typed Socket<SendData, Response> class that auto-detects the protocol, serializes JSON, queues messages until the connection opens, and dispatches typed events. The two ends share the same request/response shapes, so a real-time feature stays type-safe across the wire.

How sockets fit the framework

A socket endpoint is declared with the same decorator family as HTTP routes (@Route.socket) and resolves a container-managed controller, so the routing, validation, roles, and middleware you already use carry over. context.channel exposes subscribe, unsubscribe, publish, send, and close with the response type carried through, so broadcasts and direct replies are checked at compile time. subscribe() joins a channel and publish() fans a message out to every subscriber, which gives you room broadcasting without a separate message bus. The client mirrors the server. Socket<SendData, Response> is parameterized by the same request and response types, so the payload you send and the message you receive are the same shapes on both ends. For auth and guards, ISocketMiddleware runs the same pipeline as HTTP middleware against the socket context before the controller’s index runs.

How it works

A socket route is registered just like an HTTP route, but flagged as a socket and pinned to GET. When a client opens the connection, the matched controller’s index(context) runs once. From there, everything happens through context.channel. Unlike an HTTP controller, a socket index returns nothing. It performs side effects on the channel. The IController contract is:
See Routing for the decorator family and Controllers for how controllers are resolved.

Server: defining a socket controller

@Route.socket takes the path as its first argument and a config object as its second, the same signature as @Route.get, @Route.post, and the rest. Implement IController from @talosjs/socket and do your work in index using context.channel.

The channel API

context.channel is the typed surface for everything a socket controller does. Every payload is built with context.response (the same response builder as HTTP controllers), so the response type flows through publish and send. The full context type extends the HTTP controller context with this channel:
Because it extends the controller context, you also get context.params, context.payload, context.queries, context.user, and context.response, the same members you use in HTTP controllers.

A chat room with pub/sub

A realistic room controller uses a path parameter for the room, subscribes the connection, replies directly to the joiner, and publishes a join event to everyone else in the room. Type the context for end-to-end safety.
To leave a room or end the connection, unsubscribe and optionally close with a WebSocket close code:

Socket middleware

Connection-time interception uses ISocketMiddleware, which mirrors the HTTP IMiddleware shape against the socket context: same decorator, same handler(context) contract, same short-circuiting. Use it for auth and guards before index runs.
See Middleware for the full pipeline, ordering, and short-circuit rules.

Client: connecting with @talosjs/socket-client

Socket<SendData, Response> is a thin, typed wrapper over the browser WebSocket. The constructor takes a single URL and auto-detects the protocol: http:// becomes ws://, https:// becomes wss://, and a bare host (no scheme) is upgraded to wss://. Outgoing data is JSON-serialized for you, and incoming messages are parsed back into the typed Response.

Client methods

The two generic parameters tie the client to the server: SendData (extending RequestDataType) is what you send, and Response is the message shape you receive (wrapped as ResponseDataType<Response>). A few behaviors are worth knowing:
  • Message queuing. Calling send before the socket is open pushes the message onto an internal queue. The queue is flushed in order as soon as onopen fires, so you never have to wait for the connection by hand.
  • JSON in and out. send runs JSON.stringify on your data, and incoming frames are JSON.parsed into ResponseDataType<Response> before reaching onMessage.
  • Success and done flags. A parsed message with success true reaches onMessage. An unsuccessful one is routed to the onError handler with the parsed body instead. If a message carries done, the client closes the connection.
  • Locale support. RequestDataType includes an optional lang field, so you can attach locale information to any message you send.

Typed client usage

Mirror the server’s request and response shapes for a fully typed exchange. The payload you send matches the controller’s expected payload, and the message you receive matches the controller’s response.

Practical guidance

Call @Route.socket(path, config) with the path as the first argument and the config object second; the config never contains path. Join the channel with subscribe() before you publish(), so the connection receives the broadcasts it triggers, and check isSubscribed() before unsubscribing. Reach for send to reply to the connected client alone and publish to fan a message out to every subscriber. Type both ends. A ContextConfigType on the server and matching SendData/Response generics on the client keep payloads and messages checked across the wire. Put auth and rate limits in ISocketMiddleware so unauthorized connections are rejected before index runs. When you close, pass a standard WebSocket close code (1000 for normal closure, 1008 for policy violations) and a human-readable reason so clients can react. And don’t wait on onOpen to start sending: the client flushes queued messages on open, so calling send as soon as you have data is fine.