Overview
Elysia provides first-class support for WebSockets, enabling real-time bidirectional communication between clients and servers. WebSocket routes are defined using the .ws() method.
Basic WebSocket usage
import { Elysia } from 'elysia'
const app = new Elysia ()
. ws ( '/ws' , {
open ( ws ) {
console . log ( 'Connection opened' )
},
message ( ws , message ) {
console . log ( 'Received:' , message )
ws . send ( message )
},
close ( ws ) {
console . log ( 'Connection closed' )
}
})
. listen ( 3000 )
WebSocket lifecycle hooks
WebSocket connections have several lifecycle hooks:
open
Called when a WebSocket connection is established:
. ws ( '/ws' , {
open ( ws ) {
console . log ( 'Client connected:' , ws . id )
ws . send ( 'Welcome!' )
}
})
message
Called when a message is received from the client:
. ws ( '/ws' , {
message ( ws , message ) {
console . log ( 'Received:' , message )
// Echo back to sender
ws . send ( message )
}
})
close
Called when the connection is closed:
. ws ( '/ws' , {
close ( ws , code , reason ) {
console . log ( `Connection closed: ${ code } - ${ reason } ` )
}
})
drain
Called when the socket is ready to receive more data:
. ws ( '/ws' , {
drain ( ws ) {
console . log ( 'Socket ready for more data' )
}
})
ping and pong
Handle ping/pong frames for connection health checks:
. ws ( '/ws' , {
ping ( ws , data ) {
console . log ( 'Received ping' )
},
pong ( ws , data ) {
console . log ( 'Received pong' )
}
})
Complete example from source
From example/websocket.ts:3-25:
import { Elysia } from 'elysia'
const app = new Elysia ()
. state ( 'start' , 'here' )
. ws ( '/ws' , {
open ( ws ) {
ws . subscribe ( 'asdf' )
console . log ( 'Open Connection:' , ws . id )
},
close ( ws ) {
console . log ( 'Closed Connection:' , ws . id )
},
message ( ws , message ) {
ws . publish ( 'asdf' , message )
ws . send ( message )
}
})
. get ( '/publish/:publish' , ({ params : { publish : text } }) => {
app . server ! . publish ( 'asdf' , text )
return text
})
. listen ( 3000 , ( server ) => {
console . log ( `http:// ${ server . hostname } : ${ server . port } ` )
})
Pub/Sub with topics
WebSockets in Elysia support publish/subscribe patterns:
Subscribe to topics
. ws ( '/chat' , {
open ( ws ) {
// Subscribe to a room
ws . subscribe ( 'room-1' )
},
message ( ws , message ) {
// Publish to all subscribers of room-1
ws . publish ( 'room-1' , message )
}
})
Unsubscribe from topics
. ws ( '/chat' , {
message ( ws , message ) {
if ( message === '/leave' ) {
ws . unsubscribe ( 'room-1' )
ws . send ( 'Left room-1' )
}
}
})
Check subscription status
. ws ( '/chat' , {
message ( ws , message ) {
if ( ws . isSubscribed ( 'room-1' )) {
ws . send ( 'You are in room-1' )
}
}
})
Server-side publishing
Publish messages from HTTP endpoints to WebSocket subscribers:
const app = new Elysia ()
. ws ( '/notifications' , {
open ( ws ) {
ws . subscribe ( 'notifications' )
}
})
. post ( '/notify' , ({ body }) => {
app . server ! . publish ( 'notifications' , body . message )
return { success: true }
})
. listen ( 3000 )
Schema validation
Validate WebSocket messages using schemas:
import { Elysia , t } from 'elysia'
const app = new Elysia ()
. ws ( '/chat' , {
body: t . Object ({
message: t . String (),
username: t . String ()
}),
message ( ws , message ) {
// message is typed and validated
console . log ( ` ${ message . username } : ${ message . message } ` )
ws . send ( message )
}
})
. listen ( 3000 )
Response validation
Validate outgoing messages:
const app = new Elysia ()
. ws ( '/chat' , {
response: t . Object ({
type: t . String (),
data: t . Any ()
}),
message ( ws , message ) {
// Response will be validated
ws . send ({
type: 'message' ,
data: message
})
}
})
. listen ( 3000 )
Headers and query parameters
Access request data during WebSocket upgrade:
const app = new Elysia ()
. ws ( '/chat' , {
query: t . Object ({
room: t . String ()
}),
open ( ws ) {
// Access validated query params from ws.data
const room = ws . data . query . room
ws . subscribe ( room )
},
message ( ws , message ) {
const room = ws . data . query . room
ws . publish ( room , message )
}
})
. listen ( 3000 )
Authentication
Implement WebSocket authentication using headers:
const app = new Elysia ()
. ws ( '/private' , {
headers: t . Object ({
authorization: t . String ()
}),
beforeHandle ({ headers , error }) {
const token = headers . authorization ?. replace ( 'Bearer ' , '' )
if ( ! isValidToken ( token )) {
return error ( 401 , 'Unauthorized' )
}
},
open ( ws ) {
console . log ( 'Authenticated connection' )
}
})
. listen ( 3000 )
WebSocket configuration
Configure WebSocket behavior globally:
const app = new Elysia ({
websocket: {
idleTimeout: 30 ,
maxPayloadLength: 16 * 1024 * 1024 ,
compression: true ,
sendPingsAutomatically: true ,
publishToSelf: false
}
})
. ws ( '/ws' , {
message ( ws , message ) {
ws . send ( message )
}
})
. listen ( 3000 )
Context access
Access context data including store and decorators:
const app = new Elysia ()
. state ( 'connections' , 0 )
. decorate ( 'logger' , console )
. ws ( '/ws' , {
open ( ws ) {
ws . data . store . connections ++
ws . data . logger . log ( `Total connections: ${ ws . data . store . connections } ` )
},
close ( ws ) {
ws . data . store . connections --
}
})
. listen ( 3000 )
Binary data
Send and receive binary data:
const app = new Elysia ()
. ws ( '/binary' , {
message ( ws , message ) {
if ( Buffer . isBuffer ( message )) {
console . log ( 'Received binary data:' , message . length , 'bytes' )
ws . send ( message )
}
}
})
. listen ( 3000 )
Real-world chat application
import { Elysia , t } from 'elysia'
interface User {
id : string
username : string
room : string
}
const app = new Elysia ()
. state ( 'users' , new Map < string , User >())
. ws ( '/chat' , {
query: t . Object ({
room: t . String (),
username: t . String ()
}),
body: t . Object ({
type: t . Union ([
t . Literal ( 'message' ),
t . Literal ( 'typing' )
]),
content: t . String ()
}),
open ( ws ) {
const { room , username } = ws . data . query
const user : User = {
id: ws . id ,
username ,
room
}
ws . data . store . users . set ( ws . id , user )
ws . subscribe ( room )
// Notify others
ws . publish ( room , {
type: 'join' ,
username ,
timestamp: Date . now ()
})
},
message ( ws , message ) {
const user = ws . data . store . users . get ( ws . id ) !
ws . publish ( user . room , {
type: message . type ,
username: user . username ,
content: message . content ,
timestamp: Date . now ()
})
},
close ( ws ) {
const user = ws . data . store . users . get ( ws . id )
if ( user ) {
ws . publish ( user . room , {
type: 'leave' ,
username: user . username ,
timestamp: Date . now ()
})
ws . data . store . users . delete ( ws . id )
}
}
})
. get ( '/rooms/:room/users' , ({ params , store }) => {
const users = Array . from ( store . users . values ())
. filter ( u => u . room === params . room )
. map ( u => ({ id: u . id , username: u . username }))
return { users }
})
. listen ( 3000 )
WebSocket routes automatically handle the upgrade from HTTP to WebSocket protocol. You don’t need to manually handle the handshake.
Client connection example
// Browser client
const ws = new WebSocket ( 'ws://localhost:3000/chat?room=general&username=Alice' )
ws . onopen = () => {
console . log ( 'Connected' )
ws . send ( JSON . stringify ({
type: 'message' ,
content: 'Hello everyone!'
}))
}
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data )
console . log ( 'Received:' , data )
}
ws . onerror = ( error ) => {
console . error ( 'WebSocket error:' , error )
}
ws . onclose = () => {
console . log ( 'Disconnected' )
}
Best practices
Always validate WebSocket messages to ensure data integrity and type safety.
Unsubscribe from topics and clean up resources in the close hook to prevent memory leaks.
Validate authorization during the upgrade phase using headers or query parameters.
Use topics for scalability
Organize communication using topics/rooms rather than broadcasting to all connections.
Monitor connection limits
Track active connections and implement limits to prevent resource exhaustion.
WebSocket connections consume server resources. Implement proper connection limits, idle timeouts, and cleanup mechanisms in production.