Use TypeScript decorators to simplify working with Socket.IO in your Node.js applications.
This library provides an elegant and declarative way to define Socket.IO event listeners, emitters, middlewares, and more β all using modern TypeScript decorators.
- Installation
- Quick Start
- Decorators
- Middlewares
- Data validation
- Hooks
- Dependency Injection
- Migration 1.3.0 to 1.3.1
To get started, follow these steps:
-
Install the package:
npm install @admandev/socketio-decorator socket.io
[!NOTE] βΉοΈ Peer dependencies like
reflect-metadata
andclass-validator
may also be required depending on your use case (see Data Validation). -
Update your
tsconfig.json
to enable decorators:{ "compilerOptions": { "module": "Node16 (or more recent)", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
-
Create a Socket Controller
import { Data, ServerOn, SocketOn, SocketEmitter } from "@admandev/socketio-decorator" import { Socket } from "socket.io" export class SocketController { @ServerOn("connection") public onConnection(@CurrentSocket() socket: Socket) { console.log("Socket connected with socket id", socket.id) } @SocketOn("message") public onMessage(@CurrentSocket() socket: Socket, @Data() data: any) { console.log("Message received:", data, "from socket id:", socket.id) } // Async / Await is supported @SocketOn("hello") @SocketEmitter("hello-back") // Emit returned data as response, automatically public async onHello() { await something() return { message: "Hello you" } } }
-
Set Up the Server
In your
app.ts
file, set up the server and use the Controller:import { useSocketIoDecorator } from "@admandev/socketio-decorator" import express from "express" import http from "http" import { Server } from "socket.io" import { SocketController } from "./SocketController" const app = express() const server = http.createServer(app) const io = new Server(server) useSocketIoDecorator({ ioserver: io, controllers: [SocketController], }) server.listen(3000, () => { console.log("Server running on port 3000") })
You can also auto import controllers from a directory:
useSocketIoDecorator({ controllers: [path.join(__dirname, "/controllers/*.js")], ... })
The following decorators can be used to listen for events:
Decorator | Description | Equivalent in Basic Socket.io |
---|---|---|
@ServerOn(event: string) |
Listens for server events. | io.on(event, callback) |
@SocketOn(event: string) |
Listens for events emitted by the client. | socket.on(event, callback) |
@SocketOnce(event: string) |
Listens for events emitted by the client only once. | socket.once(event, callback) |
@SocketOnAny() |
Listens for any event emitted by the client. | socket.onAny(callback) |
@SocketOnAnyOutgoing() |
Listens for any outgoing event. | socket.onAnyOutgoing(callback) |
Equivalent in basic Socket.io: io.on(event, callback)
Listens for server events.
Usage :
@ServerOn("connection")
public onConnection(@CurrentSocket() socket: Socket) {
console.log("Socket connected with socket id", socket.id)
}
Equivalent in basic Socket.io: socket.on(event, callback)
Listens for events emitted by the client.
Usage :
@SocketOn("message")
public onMessage(@Data() data: any) {
console.log("Message received:", data)
}
Equivalent in basic Socket.io: socket.once(event, callback)
Listens for events emitted by the client only once.
Usage :
@SocketOnce("message")
public onMessage(@Data() data: any) {
console.log("Message received:", data)
}
Equivalent in basic Socket.io: socket.onAny(callback)
Listens for any event emitted by the client.
Usage :
@SocketOnAny()
public onAnyEvent(@EventName() event: string, @Data() data: any) {
console.log("Any event received:", event, data)
}
Equivalent in basic Socket.io: socket.onAnyOutgoing(callback)
Listens for any outgoing event
Usage :
@SocketOnAnyOutgoing()
public onAnyOutgoingEvent(@EventName() event: string, @Data() data: any) {
console.log("Any outgoing event received:", event, data)
}
The following decorators can be used to emit events to the client:
Decorator | Description | Equivalent in Basic Socket.io |
---|---|---|
@ServerEmitter(event?: string, to?: string) |
Emits event to all clients. | io.emit(event, data) |
@SocketEmitter(event:? string) |
Emits event to specific client. | socket.emit(event, data) |
-
Basic usage
The return value of the method is sent as the data of the event.
@SocketOn("get-latest-message") @SocketEmitter("message") public sendMessage() { return { message: "Hello, world!" } }
The above code will emit a
message
event with the following data as response to the client :{ "message": "Hello, world!" }
-
Emitting options
-
You can also specify options for the emitted event by returning an
EmitterOption
object.import { EmitterOption, SocketEmitter } from "@admandev/socketio-decorator" import { Socket } from "socket.io" @SocketOn("chat-message") @SocketEmitter() // No event name specified public sendMessage(@CurrentSocket() socket: Socket): EmitterOptions { const isAllowedToSend = isUserAllowedToSendMessage(socket) return new EmitterOption({ to: "room1", message: "newMessage", // Event name set here data: { message: "Hello, world!" }, disableEmit: !isAllowedToSend, }) }
The above code will emit a
newMessage
event to theroom1
room. The event will only be emitted if theisUserAllowedToSendMessage
function returnstrue
. . -
If you return an array of
EmitterOption
objects, an event will be emitted for eachEmitterOption
items.@SocketOn("multiple-events") @ServerEmitter() onMultipleEvents(@CurrentSocket() socket: Socket) { socket.join("multiple-events") const events: EmitterOption[] = [ new EmitterOption({ to: socket.id, message: "event-1", data: { message: "This is event 1" } }), new EmitterOption({ to: "multiple-events", message: "event-2", data: { message: "This is events-2" } }), ] return events }
The above code will emit two events:
event-1
andevent-2
.event-1
will be emitted to the client with theid
of the socket andevent-2
will be emitted to themultiple-events
room.
Emitter options The
EmitterOption
object has the following properties:Property Type Required Description to
string No (if the decorator provides this) The target to emit the event to. message
string No (if the decorator provides this) The event name to emit. data
any Yes The data to emit. disableEmit
boolean No If true
, the event will not be emitted. -
-
Emitting falsy value If the method returns a falsy value (false, null undefined, 0, ...), the event will not be emitted.
Equivalent in basic Socket.io: io.emit(event, data)
or io.to(to).emit(event, data)
Emits events to all connected clients or to a specific room if the to
parameter is provided.
Usages :
@ServerEmitter("newMessage", "room1")
public sendMessage() {
return { message: "Hello, world!" }
}
@ServerEmitter()
public sendMessage() {
return new EmitterOption({
to: "room1",
message: "newMessage",
data: { message: "Hello, world!" },
})
}
Equivalent in basic Socket.io: socket.emit(event, data)
Emits event to the current client.
Warning
If the event
parameter is not provided in decorator, it must be provided in the EmitterOption
object.
Warning
This decorator must be used with a listener decorator (ServerOn or SocketOn) to work.
Usage :
@SocketOn("join-room")
@SocketEmitter("room-joined")
public joinRoom(@CurrentSocket() socket: Socket) {
socket.join("myRoom")
return {
info: `You have successfully joined room myRoom`,
roomId: "myRoom"
}
}
@SocketOn("join-room")
@SocketEmitter()
public joinRoom(@CurrentSocket() socket: Socket) {
socket.join("myRoom")
return new EmitterOption({
to: socket.id,
message: "room-joined",
data: {
info: `You have successfully joined room myRoom`,
roomId: "myRoom"
},
})
}
The following decorators can be used to inject parameters into the event handler methods:
Decorator | Description |
---|---|
@CurrentSocket() |
Injects the current socket instance that is handling the message. |
@Data(dataIndex?: number) |
Injects the data sent by the client |
@EventName() |
Injects the name of the event message that triggered the handler. |
@CurrentUser() |
Injects the current user object. |
Injects the current socket instance that is handling the message.
Usage :
@SocketOn("joinGame")
public onJoinGame(@CurrentSocket() socket: Socket) {
socket.join("gameRoom")
}
Injects the data sent by the client.
Usage :
@SocketOn("message")
public onMessage(@Data() data: MessageData) {
console.log("Message received:", data.message)
}
You can also specify the index of the data in the socket message if you want to inject a specific part of the data:
@SocketOn("chat-message")
public onChatMessage(@Data(0) message: string, @Data(1) roomId: string) {
console.log(`Received message: "${message}" for room: ${roomId}`)
}
This is useful when the client sends multiple arguments:
// Client side
socket.emit("chat-message", "Hello everyone!", "gaming-lobby")
Injects the name of the event message that triggered the handler.
Usage :
@SocketOn("user-joined")
@SocketOn("user-left")
public trackUserActivity(@EventName() event: string) {
const action = event === "user-joined" ? "joined the chat" : "left the chat"
console.log(`User ${action}`)
}
Injects the current user object into an event handler parameter.
Usage :
-
Create the
currentUserProvider
In the
app.ts
file, create a function that returns the current user object:useSocketIoDecorator({ ..., currentUserProvider: async (socket: Socket) => { const token = socket.handshake.auth.token return await userServices.getUserByToken(token) }, })
-
Use the
CurrentUser
decoratoarIn the event handler, use the
CurrentUser
decorator to get the current user object:import { CurrentUser, SocketOn } from "@admandev/socketio-decorator" @SocketOn("message") public onMessage(@CurrentUser() user: User) { console.log("Message received from user:", user.name) }
Decorator | Description |
---|---|
@UseSocketMiddleware(...ISocketMiddleware[]) |
Applies one or more socket middleware to the event handler or controller class. |
Applies one or more socket middlewares to the event handler or controller class.
Usage :
First create a socket middleware before choosing one of next steps.
-
Use it on an event handler method:
@SocketOn("message") @UseSocketMiddleware(MyMiddleware1, MyMiddleware2) public onMessage() { console.log("Message received") }
In this case, the
MyMiddleware1
andMyMiddleware2
will be called before theonMessage
event handler is executed. -
Use it on a controller class:
@UseSocketMiddleware(MyMiddleware) export class MyController { @SocketOn("event1") public onEvent1() { console.log("Event 1 received") } @SocketOn("event2") public onEvent2() { console.log("Event 2 received") } }
In this case, the
MyMiddleware
will be applied to all event handlers in theMyController
class.[!NOTE] This decorator is applied to socket listener handlers only (
@SocketOn
,@SocketOnce
,@SocketOnAny
, ...). It does not apply to server listeners (@ServerOn
) or emitters.
You can use middlewares to execute code before an event is handled. Middlewares can be used to perform tasks such as authentication or logging.
A Server Middleware is executed for each incoming connection.
-
Create a Middleware
export class MyServerMiddleware implements IServerMiddleware { use(socket: Socket, next: (err?: unknown) => void) { console.log("You can perform tasks here before the event is handled") next() } }
The
use
method is called before any event is handled. You can perform any tasks here and callnext()
to proceed with the event handling. -
Register the Middleware
Update the
app.ts
file to register the middleware:useSocketIoDecorator({ ..., serverMiddlewares: [MyServerMiddleware], // Add the middleware here })
A Socket Middleware is like Server Middleware but it is called for each incoming packet.
-
Create a Middleware
import { ISocketMiddleware } from "@admandev/socketio-decorator" import { Event, Socket } from "socket.io" export class MySocketMiddleware implements ISocketMiddleware { use(socket: Socket, [event, ...args]: Event, next: (err?: Error) => void): void { console.log(`MySocketMiddleware triggered from ${event} event`) next() } }
-
Use the Middleware
Now you can use the socket middleware in 2 ways:
-
Globally: This will apply the middleware to all events in your application. Update the
app.ts
file to register the middleware:useSocketIoDecorator({ ..., socketMiddlewares: [MySocketMiddleware], // Add the middleware here })
-
Per event: You can also use the middleware for a specific event by using the @UseSocketMiddleware decorator.
-
You can create a middleware to handle errors that occur during event handling and above middlewares.
-
Create an Error Middleware
import { IErrorMiddleware } from "@admandev/socketio-decorator" import { Socket } from "socket.io" export class MyErrorMiddleware implements IErrorMiddleware{ handleError (error: any, socket?: Socket) { // Handle the error here console.log('Error middleware: ', error) } }
-
Register the Middleware
Update the
app.ts
file to register the middleware:useSocketIoDecorator({ ..., errorMiddleware: MyErrorMiddleware, // Add the unique error middleware here })
You can use the class-validator
library to validate the data received from the client and be sure that required fields are present and have the correct type.
-
Install the following libraries
npm install class-validator class-transformer reflect-metadata
-
Import the
reflect-metadata
libraryAdd the following line at the top of your
app.ts
file:import "reflect-metadata"
-
Be sure to enable the
emitDecoratorMetadata
option in yourtsconfig.json
file{ "compilerOptions": { "emitDecoratorMetadata": true } }
-
Enable the validation option in the
useSocketIoDecorator
configuseSocketIoDecorator({ ..., dataValidationEnabled: true })
-
Create and use a class with validation rules
import { IsString } from "class-validator" export class MessageData { @IsString() @IsNotEmpty() message: string }
Use the class in the event handler:
@SocketOn("message") public onMessage(@Data() data: MessageData) { console.log("Message received:", data.message) }
If the data does not match the validation rules, an error will be thrown before the event handler is called.
Warning
We recommend using the error handling middleware to catch and handle validation errors.
You can disable validation for a specific handler by setting the disableDataValidation
option to true
:
@SocketOn("message", { disableDataValidation: true })
public onMessage(@Data() data: MessageData) {
...
}
Data validation works only on socket listeners (not server listeners or emitters).
Here is the default value for the disableDataValidation
option:
@SocketOn
-false
@SocketOnce
-false
@SocketOnAny
-true
- If you want to validate the data, you need to set the option tofalse
@SocketOnAnyOutgoing
-true
because it is not an incoming event from the client
For more information on data validation, see the class-validator documentation.
Hooks in Socketio Decorator are functions that provides some data.
The useIoServer
is the simpliest hook that provides the io
socketio server object.
import { useIoServer } from "@admandev/socketio-decorator"
import { Server } from "socket.io"
const io: Server = useIoServer()
The useUserSocket
hook allows you to retrieve a specific connected socket instance based on a search argument (e.g., user ID).
-
Setup the
searchUserSocket
functionIn the
app.ts
file, provide a function that searches for a user socket based on an argument:useSocketIoDecorator({ ..., // Here we decide that the search argument is the user ID but you can use any other argument type searchUserSocket: async (userId: string) => { const allSockets = Array.from(io.sockets.sockets.values()) return allSockets.find(socket => socket.user.id === userId) || null }, })
-
Use the
useUserSocket
hook anywhereimport { useUserSocket } from "@admandev/socketio-decorator" import { Socket } from "socket.io" const userSocket: Socket | null = await useUserSocket(userId)
Socketio Decorator supports dependency injection using a DI library. You can inject services into your controllers and middlewares.
To allow Socketio Decorator to work with your DI system, you need to provide the Container
object to the useSocketIoDecorator
options.
import { Container } from "typedi"
useSocketIoDecorator({
...,
iocContainer: Container,
})
Note
Your Container object must provide the get
method to resolve dependencies.
Check out the full example using Express: π Example on GitHub
If you run into any issues or have suggestions, feel free to open an issue on GitHub:
π Socket.io Decorator Issues
Thank you for using Socketio Decorator
Starting from version 1.3.1, you need to use the parameter injection decorators (@CurrentSocket()
, @Data()
, @EventName()
) to access the socket, data, and event name in your handlers:
// Before (1.3.0)
@SocketOn("message")
public onMessage(socket: Socket, data: any) {
console.log("Message from:", socket.id, "data:", data)
}
// After (1.3.1+)
@SocketOn("message")
public onMessage(@CurrentSocket() socket: Socket, @Data() data: any) {
console.log("Message from:", socket.id, "data:", data)
}