npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@admandev/socketio-decorator

v1.4.1

Published

This library allows you to use Socket.io with TypeScript decorators, simplifying the integration and usage of Socket.io in a TypeScript environment.

Downloads

143

Readme

Socketio Decorator

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.

📚 Table of Contents

Installation

To get started, follow these steps:

  1. Install the package:

    npm install @admandev/socketio-decorator socket.io

    [!NOTE] ℹ️ Peer dependencies like reflect-metadata and class-validator may also be required depending on your use case (see Data Validation).

  2. Update your tsconfig.json to enable decorators:

    {
        "compilerOptions": {
            "module": "Node16 (or more recent)",
            "experimentalDecorators": true,
            "emitDecoratorMetadata": true
        }
    }

Quick Start

  1. 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"
            }
        }
    
    }
  2. 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")],
        ...
    })

Decorators

Listening for Events

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) |

Example


@SeverOn(event: string)

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)
}

@SocketOn(event: string)

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)
}

@SocketOnce(event: string)

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)
}

@SocketOnAny()

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)
}

@SocketOnAnyOutgoing()

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)
}

Emitting Events

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) |

How to use

  1. 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!"
        }
  2. 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 the room1 room. The event will only be emitted if the isUserAllowedToSendMessage function returns true. .

    • If you return an array of EmitterOption objects, an event will be emitted for each EmitterOption 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 and event-2. event-1 will be emitted to the client with the id of the socket and event-2 will be emitted to the multiple-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. |

  3. Emitting falsy value If the method returns a falsy value (false, null undefined, 0, ...), the event will not be emitted.

Examples


@ServerEmitter(event?: string, to?: string)

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!" },
    })
}

@SocketEmitter(event?: string)

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"
        },
    })
}

Room decorators

The following decorators can be used to manage socket.io rooms:

| Decorator | Description | Equivalent in Basic Socket.io | |-----------|-------------|-------------------------------| | @OnRoomCreated(roomName?: string) | Listens for room creation events. | namespace.adapter.on("create-room", callback) | | @OnRoomDeleted(roomName?: string) | Listens for room deletion events. | namespace.adapter.on("delete-room", callback) | | @OnRoomJoined(roomName?: string) | Listens for room joined events. | namespace.adapter.on("join-room", callback) | | @OnRoomLeft(roomName?: string) | Listens for room left events. | namespace.adapter.on("leave-room", callback) | | @SocketRoom(roomName?: string) | Injects a specific room or all rooms the current socket is in. | / |

[!NOTE] Socket ID room events are automatically filtered out and will not trigger the listener.

To learn more about room management, see UseRoomStore hook and UseRoom hook.

Room listeners setup

Classes using these decorators must be registered in the useSocketIoDecorator config:

class ChatRoomEvents {
    @OnRoomCreated()
    public onRoomCreated(roomName: string) {
        console.log(`Room ${roomName} has been created`)
    }
}

useSocketIoDecorator({
    roomEventListeners: [ChatRoomEvents], // Or [path/to/directory/*.js]
    ...
})

Wildcard patterns

All room decorators support wildcard patterns using * to match dynamic room names. This allows you to listen for events on multiple rooms with a single handler.

class ChatRoomEvents {
    // Listens to all chat rooms (chat-1, chat-2, chat-general, etc.)
    @OnRoomCreated("chat-*")
    public onChatRoomCreated(roomName: string) {
        console.log(`Chat room ${roomName} was created`)
    }

    // Listens to all notification rooms (user-notifications, admin-notifications, etc.)
    @OnRoomJoined("*-notifications")
    public onNotificationRoomJoined(roomName: string, socket: Socket) {
        console.log(`Socket ${socket.id} joined notification room ${roomName}`)
    }

    // Listens to all game lobbies (game-1-lobby, game-tournament-lobby, etc.)
    @OnRoomLeft("game-*-lobby")
    public onGameLobbyLeft(roomName: string, socket: Socket) {
        console.log(`Socket ${socket.id} left game lobby ${roomName}`)
    }
}

Namespace support

All room decorators support namespace. You can specify the namespace to listen for events on a specific namespace.

// This will trigger only in the /my-namespace namespace
@OnRoomCreated("lobby", { namespace: "/my-namespace" })
public onLobbyRoomCreated(roomName: string) {
    console.log(`Room ${roomName} has been created`)
}

Examples


@OnRoomCreated(roomName?: string)

Equivalent in basic Socket.io: namespace.adapter.on("create-room", callback)

Listens for room creation events. If no room name is provided, the listener will be triggered for any room.

This decorator requires to be used on a handler with the signature:

(roomName: string) => any

Usage :

// Trigger only for the lobby room creation event
@OnRoomCreated("lobby")
public onLobbyRoomCreated(roomName: string) {
    console.log(`Room ${roomName} has been created`)
}
// Trigger for any room creation event
@OnRoomCreated()
public onAnyRoomCreated(roomName: string) {
    console.log(`Room ${roomName} has been created`)
}

@OnRoomDeleted(roomName?: string)

Equivalent in basic Socket.io: namespace.adapter.on("delete-room", callback)

Listens for room deletion events. If no room name is provided, the listener will be triggered for any room.

This decorator requires to be used on a handler with the signature:

(roomName: string) => any

Usage :

// Trigger only for the lobby room deletion event
@OnRoomDeleted("lobby")
public onLobbyRoomDeleted(roomName: string) {
    console.log(`Room ${roomName} has been deleted`)
}
// Trigger for any room deletion event
@OnRoomDeleted()
public onAnyRoomDeleted(roomName: string) {
    console.log(`Room ${roomName} has been deleted`)
}

@OnRoomJoined(roomName?: string)

Equivalent in basic Socket.io: namespace.adapter.on("join-room", callback)

Listens for specific room joined events. If no room name is provided, the listener will be triggered for any room.

This decorator requires to be used on a handler with the signature:

(roomName: string, socket: Socket) => any

Usage :

// Trigger only for the lobby room joined event
@OnRoomJoined("lobby")
public onLobbyRoomJoined(roomName: string, socket: Socket) {
    console.log(`Socket ${socket.id} joined room ${roomName}`)
}
// Trigger for any room joined event
@OnRoomJoined()
public onAnyRoomJoined(roomName: string, socket: Socket) {
    console.log(`Socket ${socket.id} joined room ${roomName}`)
}

@OnRoomLeft(roomName?: string)

Equivalent in basic Socket.io: namespace.adapter.on("leave-room", callback)

Listens for specific room left events. If no room name is provided, the listener will be triggered for any room.

This decorator requires to be used on a handler with the signature:

(roomName: string, socket: Socket) => any

Usage :

// Trigger only for the lobby room left event
@OnRoomLeft("lobby")
public onLobbyRoomLeft(roomName: string, socket: Socket) {
    console.log(`Socket ${socket.id} left room ${roomName}`)
}
// Trigger for any room left event
@OnRoomLeft()
public onAnyRoomLeft(roomName: string, socket: Socket) {
    console.log(`Socket ${socket.id} left room ${roomName}`)
}

@SocketRoom(roomName?: string)

Injects a specific room or all rooms the current socket is in.

Usage :

  1. Inject all rooms the socket is in

    When used without a parameter, @SocketRoom() injects an array of all rooms the current socket is in.

    @SocketOn("message")
    public onMessage(@SocketRoom() rooms: ChatRoom[]) {
        console.log(`Socket is in ${rooms.length} rooms`)
    }
  2. Inject a specific room

    When used with a room name parameter, @SocketRoom("roomName") injects the specific room object if the socket is in that room, otherwise it injects null.

    @SocketOn("message")
    public onMessage(@SocketRoom("roomName") room: ChatRoom | null) {
        if (room) {
            console.log("Socket is in room:", room.name)
        } else {
            console.log("Socket is not in the room")
        }
    }
  3. Handler with required room

    If you want to ensure that the socket is in a specific room before handling the event, you can use the required option. If the socket is not in the specified room, an SiodRequiredRoomError will be thrown.

    @SocketOn("message")
    public onMessage(@SocketRoom("roomName", { required: true }) room: ChatRoom) {
        console.log("For sure, socket is in the room:", room.name)
    }

Parameter injection decorators

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. | | @SocketData(dataKey?: string) | Injects socket data value (socket.data[dataKey]). |

Examples


@CurrentSocket()

Injects the current socket instance that is handling the message.

Usage :

@SocketOn("joinGame")
public onJoinGame(@CurrentSocket() socket: Socket) {
    socket.join("gameRoom")
}

@Data(dataIndex?: number)

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")

@EventName()

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}`)
}

@CurrentUser()

Injects the current user object into an event handler parameter.

Usage :

  1. 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)
        },
    })
  2. Use the CurrentUser decoratoar

    In 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)
    }

@SocketData(dataKey?: string)

Equivalent in basic Socket.io: socket.data[dataKey]

Injects a socket data attribute value into an event handler parameter. SocketData decorator allow you to store custom data on a per-socket basis, which persists for the lifetime of the socket connection.

Usage :

  1. Inject the entire SocketDataStore

    When used without a parameter, @SocketData() injects a SocketDataStore instance that provides methods to manage socket data attribute:

    @SocketOn("save-user-preferences")
    public savePreferences(@SocketData() dataStore: SocketDataStore) {
        // Store user preferences on this socket
        dataStore.setData("theme", "dark")
        dataStore.setData("language", "fr")
           
        console.log("Preferences saved for this socket")
    }
    
    @SocketOn("get-user-preferences")
    @SocketEmitter("preferences")
    public getPreferences(@SocketData() dataStore: SocketDataStore) {
        return {
            theme: dataStore.getData("theme"),
            language: dataStore.getData("language")
        }
    }

    SocketDataStore API

    The SocketDataStore class provides the following methods:

    | Method | Parameters | Returns | Description | |--------|------------|---------|-------------| | getData(key) | key: string | any \| null | Retrieves the value of a socket data attribute. Returns null if the key doesn't exist. | | setData(key, value) | key: string, value: any | void | Sets a socket data with the specified key and value. | | removeData(key) | key: string | void | Removes a socket data attribute by key. | | hasData(key) | key: string | boolean | Checks if a socket data exists for the specified key. |

    Type Safety with Generics

    You can type the SocketDataStore methods using generics by defining an interface that describes your socket data structure:

    // Define your socket data type
    interface MyStoreType {
        userId: number
        theme: 'light' | 'dark'
        language: string
    }
    
    @SocketOn("example")
    public example(@SocketData() dataStore: SocketDataStore<MyStoreType>) {
        // ❌ Type error - "unknownKey" is not a valid key of MyStoreType
        dataStore.getData("unknownKey")
           
        // ✅ Correct - "userId" is a valid key
        dataStore.getData("userId") // Returns: number | null
           
        // ❌ Type error - Argument of type 'string' is not assignable to parameter of type 'number'
        dataStore.setData("userId", "not-a-number")
           
        // ✅ Correct - proper type
        dataStore.setData("userId", 123) // ✅ Works correctly
        dataStore.setData("theme", "dark") // ✅ Only 'light' | 'dark' allowed
        dataStore.setData("language", "en") // ✅ String type as expected
    }

    With typed SocketDataStore<MyStoreType>, you get:

    • Autocompletion: IDE suggests only valid keys from your interface
    • Type checking: Values must match the expected types
    • Compile-time errors: Catch mistakes before runtime
  2. Inject a specific data attribute value

    When used with a key parameter, @SocketData("key") directly injects the value of that specific data attribute:

    @SocketOn("update-theme")
    public updateTheme(@SocketData("theme") currentTheme: string) {
        console.log("Current theme:", currentTheme) // Will be null if not set
    }

    Important notes

    • Socket data attributes are stored per socket connection and persist for the lifetime of that connection
    • When a socket disconnects, all associated data attributes are automatically cleared
    • This is built on top of Socket.IO's native socket.data property
    • Data attributes are not shared between different socket connections, even for the same user

Other decorators

| Decorator | Description | |-------------------------|----------------------------------------------------------| | @UseSocketMiddleware(...ISocketMiddleware[]) | Applies one or more socket middleware to the event handler or controller class. | | @SocketNamespace(namespace: string) | Defines a namespace for a socket controller | | @MiddlewareOption(options: MiddlewareOptionType) | Applies options to a middleware. | | @Throttle(limit: number, timeWindowMs: number) | Applies rate limiting to a controller or event handler. ( See Rate Limiting) |

Examples


@UseSocketMiddleware(...ISocketMiddleware[])

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.

  1. Use it on an event handler method:

    @SocketOn("message")
    @UseSocketMiddleware(MyMiddleware1, MyMiddleware2)
    public onMessage() {
        console.log("Message received")
    }

    In this case, the MyMiddleware1 and MyMiddleware2 will be called before the onMessage event handler is executed.

  2. 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 the MyController class.

    [!NOTE] This decorator is applied to socket listener handlers only (@SocketOn, @SocketOnce, @SocketOnAny, ...). It does not apply to server listeners (@ServerOn) or emitters.


@SocketNamespace(namespace: string)

Equivalent in basic Socket.io: io.of(namespace)

Defines a namespace for a socket controller. All socket events in this controller will be handled within this namespace. Learn more about Socket.IO namespaces.

Usage :

@SocketNamespace("/my-namespace")
export class MyNamespaceController {
    @SocketOn("message")
    public onMessage(@Data() data: MessageRequest) {
        console.log("Message in my-namespace", data.message)
    }
}

[!WARNING] The namespace must start with "/" or it will throw a SiodDecoratorError.


@MiddlewareOption(options: MiddlewareOptionType)

Applies options to a middleware.

Middleware options

| Property | Type | Description | |----------|------|-------------| | namespace | string | The namespace to which the middleware should be applied. |

Middlewares

You can use middlewares to execute code before an event is handled. Middlewares can be used to perform tasks such as authentication or logging.

Server Middleware

A Server Middleware is executed for each incoming connection.

  1. 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 call next() to proceed with the event handling.

  2. Register the Middleware

    Update the app.ts file to register the middleware:

    useSocketIoDecorator({
        ...,
        serverMiddlewares: [MyServerMiddleware], // Add the middleware here
    })

Socket Middleware

A Socket Middleware is like Server Middleware but it is called for each incoming packet.

  1. 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()
        }
    }
  2. 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.

Namespace middleware

You can scope a middleware to a specific namespace using the @MiddlewareOption decorator. This is particularly useful when using the @SocketNamespace decorator to organize your controllers by namespaces.

@MiddlewareOption({ namespace: "/orders" })
class OrderMiddleware implements IServerMiddleware {
    public use(socket: Socket, next: (err?: unknown) => void) {
        // This middleware will only be executed for connections to the "/orders" namespace
        console.log("New connection to /orders namespace")
        next()
    }
}

In this example:

  • The OrderMiddleware will only be executed for connections to the "/orders" namespace
  • Other namespaces (or the default namespace) will not trigger this middleware
  • This works for both Server and Socket middlewares (globally only)

Error handling middleware

You can create a middleware to handle errors that occur during event handling and above middlewares.

  1. 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)
        }
    }
  2. Register the Middleware

    Update the app.ts file to register the middleware:

    useSocketIoDecorator({
         ...,
         errorMiddleware: MyErrorMiddleware, // Add the unique error middleware here
    })

Rate Limiting

You can use rate limiting to control how many requests a client can make within a specific time window. This helps protect your application from abuse and ensures fair resource usage.

Global Rate Limiting

To enable rate limiting globally for all controllers:

useSocketIoDecorator({
    ...,
    throttleConfig: {
        rateLimitConfig: {
            limit: 100,        // Maximum number of requests
            timeWindowMs: 60000 // Time window in milliseconds (1 minute)
        },
        // Optional: cleanup interval for expired throttle data (default: 1 hour)
        cleanupIntervalMs: 3600000,
        // Optional: custom storage implementation
        store: InMemoryThrottleStorage // By default, uses in-memory storage
        getUserIdentifier: (socket) => {
            // Return any unique identifier for the user
            return "By default, it uses the socket id"
        }
    }
})

Class-Level Rate Limiting

Apply rate limiting to all socket events in a controller:

@Throttle(10, 60000) // Max 10 requests per minute
class UserController {
    @SocketOn("update-profile")
    public updateProfile() { }

    @SocketOn("change-settings")
    public changeSettings() { }

    @SocketOn("upload-avatar")
    public uploadAvatar() { }
}

Method-Level Rate Limiting

Use the @Throttle decorator to apply rate limiting to specific methods:

class ChatController {
    @SocketOn("message")
    @Throttle(5, 1000) // Max 5 requests per second
    public sendMessage(@Data() message: string) {
        console.log("Message received:", message)
    }
}

Priority and Scope

Rate limiting follows a hierarchy where more specific configurations override broader ones:

  1. Method-level (Highest Priority): @Throttle decorator on individual methods
  2. Class-level (Medium Priority): @Throttle decorator on controller classes
  3. Global (Lowest Priority): throttleConfig in useSocketIoDecorator
// Global configuration (lowest priority)
useSocketIoDecorator({
    throttleConfig: {
        rateLimitConfig: { limit: 100, timeWindowMs: 60000 }
    }
})

// Class-level configuration (medium priority)
@Throttle(20, 60000) // Overrides global config for this controller
class ChatController {
    
    @SocketOn("message")
    public sendMessage() {
        // Uses class-level: 20 requests per minute
    }

    @SocketOn("upload")
    @Throttle(5, 300000) // Method-level (highest priority)
    public uploadFile() {
        // Uses method-level: 5 requests per 5 minutes
        // Overrides both class and global configs
    }
}

Custom User Identification

By default, rate limiting uses the socket ID to identify clients. However, since socket IDs change when users reconnect, you might want to use a more persistent identifier (e.g., user ID, session ID). You can configure this through the getUserIdentifier option:

useSocketIoDecorator({
    throttleConfig: {
       ...,
        // Custom user identification function
        getUserIdentifier: (socket) => {
            return "Return any unique identifier for the user"
        }
    }
})

The getUserIdentifier function:

  • Receives the socket instance as parameter
  • Should return a string or Promise

Error Handling

Rate limit errors are thrown as SiodThrottleError. Handle them using an error middleware:

class ErrorMiddleware implements IErrorMiddleware {
    handleError(error: unknown) {
        if (error instanceof SiodThrottleError) {
            console.error("Rate limit exceeded. Retry in:", error.remainingTime, "ms")
        }
    }
}

Important Notes

  • Rate limits are applied per client ID
  • Each event has its own independent rate limit counter
  • Class-level rate limits can be overridden by method-level decorators
  • Global configuration applies to all controllers without explicit @Throttle decorators
  • Rate limit data is automatically cleaned up based on cleanupIntervalMs

Custom Storage Implementation

By default, rate limiting data is stored in memory using InMemoryThrottleStorage. However, you can implement your own storage solution (e.g., Redis, Database) by implementing the IThrottleStorage interface.

Example implementation using Redis:

class RedisThrottleStorage implements IThrottleStorage {
    private static readonly redis = new Redis("You url")
    private readonly prefix = "throttle"

    // Get the throttle entry for a specific client and event
    public async get(clientId: string, event: string): Promise<ThrottleEntry | undefined> {
        try {
            const key = this.getKey(clientId, event)
            const data = await RedisThrottleStore.redis.get(key)

            if (!data) {
                return undefined
            }

            return JSON.parse(data) as ThrottleEntry
        } catch (error) {
            console.error("Redis get error:", error)
            return undefined
        }
    }

    // Set / update the throttle entry for a specific client and event
    public async set(clientId: string, event: string, entry: ThrottleEntry): Promise<void> {
        try {
            const key = this.getKey(clientId, event)
            const ttl = Math.max(0, entry.resetTime - Date.now()) // in ms

            // Save the entry with automatic expiration
            if (ttl > 0) {
                await RedisThrottleStore.redis.set(key, JSON.stringify(entry), "PX", ttl)
            }
        } catch (error) {
            console.error("Redis set error:", error)
        }
    }

    // Cleanup method to remove expired entries
    public async cleanup(): Promise<void> {
        // Redis handles expiration automatically with TTL → nothing to do here.
        // But we can flush if needed (optional)
        // await this.redis.flushall()
        return Promise.resolve()
    }

    private getKey(clientId: string, event: string): string {
        return `${this.prefix}:${clientId}:${event}`
    }
}

Then configure it in your application:

useSocketIoDecorator({
    ...,
    throttleConfig: {
        store: RedisThrottleStorage // Your custom storage class
    }
})

Data validation

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.

Setup

  1. Install the following libraries

    npm install class-validator class-transformer reflect-metadata
  2. Import the reflect-metadata library

    Add the following line at the top of your app.ts file:

    import "reflect-metadata"
  3. Be sure to enable the emitDecoratorMetadata option in your tsconfig.json file

    {
        "compilerOptions": {
            "emitDecoratorMetadata": true
        }
    }
  4. Enable the validation option in the useSocketIoDecorator config

    useSocketIoDecorator({
        ...,
        dataValidationEnabled: true
    })
  5. 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.

Disable validation for a specific handler

You can disable validation for a specific handler by setting the disableDataValidation option to true:

@SocketOn("message", { disableDataValidation: true })
public onMessage(@Data() data: MessageData) {
    ...
}

Default enabled validation

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 to false
  • @SocketOnAnyOutgoing - true because it is not an incoming event from the client

Learn more about data validation

For more information on data validation, see the class-validator documentation.

Application Events

Application events provide an internal event bus for cross-service / class communication within your server. Unlike Socket.IO events that communicate over the network, application events are server-side only and enable decoupled communication between different parts of your application.

Why use Application Events?

Application events are particularly useful for:

  • Cross-Service communication: Enable different services to communicate without direct references (e.g., OrderController notifying InventoryService and NotificationService)

  • Decoupled architecture: Create loosely coupled components that can interact through events

  • Event-driven actions: Trigger application-wide actions when Socket.IO events occur (e.g., emit an app event after processing a socket message)

How It Works

The Application Events system uses two main decorators:

| Decorator | Description | |-----------|-------------| | @AppOn(eventName: string) | Registers a method as an application event listener | | @AppEmit(eventName: string) | Emits an application event when a method executes |

When an event is emitted, all registered listeners for that event are called asynchronously with an AppEventContext object containing the event details.

How to use them?

@AppOn(eventName: string)

Description: Registers a method as an application event listener

Method signature required: (context: AppEventContext) => unknown | Promise<unknown>

Usage:

  1. Create a class with the event listener

    type OrderData = {
        orderId: string
        items: OrderItem[]
        total: number
        createdAt: Date
    }
    
    // App event listener that responds to order creation
    class InventoryService {
        @AppOn("order-created")
        public updateInventory(context: AppEventContext) {
            const order = context.data as OrderData
                       
            // Inventory update logic
        }
    }
    
    // Another listener for the same event
    class NotificationService {
        @AppOn("order-created")
        public notifyWarehouse(context: AppEventContext) {
            const order = context.data as OrderData
    
            // Available when the event is triggered from a socket handler
            const socket = context.ioContext?.currentSocket
               
            // Warehouse notification logic
        }
    }
  2. Register them in the useSocketIoDecorator config

    useSocketIoDecorator({
        ...,
        appEventListeners: [InventoryService, NotificationService] // Or [path/to/directory/*.js]
    })

Key features:

  • Multiple listeners can subscribe to the same event
  • Listeners execute independently and asynchronously
  • Method receives an AppEventContext object with event details

@AppEmit(eventName: string)

Description: Emits an application event when the decorated method executes

Usage:

// Emits from a Socket.IO listener
class OrderController {
    @SocketOn("create-order")
    @AppEmit("order-created")
    public createOrder(@Data() orderData: any) {
        console.log("Creating order from socket event")
        
        // Order creation logic
        const order: OrderData = {
            orderId: "ORD-123",
            items: orderData.items,
            total: orderData.total,
            createdAt: new Date()
        }
        
        // This return value becomes the data available in listeners context
        return order
    }
}

// Emits from a simple class method
class OrderService {
    @AppEmit("order-created")
    public createOrder(orderData: any) {
        console.log("Creating order from service")
        
        // Order creation logic
        const order = ...
        
        // This return value becomes the data available in listeners context
        return order
    }
}

Note: No need to register the event emitters in the useSocketIoDecorator config.

Key features:

  • Method's return value becomes the event data in listeners context
  • Can be combined with Socket.IO decorators
  • Can be used with any class method
  • Event is emitted to all registered listeners

AppEventContext Interface

The AppEventContext object passed to event listeners contains:

| Property | Type | Description | |----------|------|-------------| | eventName | string | The name of the application event that triggered the listener | | data | unknown | The data associated with the event (return value from @AppEmit method) | | ioContext | object (optional) | Socket.IO context when event is triggered from a socket handler |

ioContext properties (when available):

| Property | Type | Description | |----------|------|-------------| | currentSocket | Socket \| null | The current Socket.IO socket instance | | eventName | string | The original Socket.IO event name | | eventData | unknown[] | The original Socket.IO event arguments |

Use application event bus dynamically

You can also use the application event bus dynamically by using the useAppEventBus hook. See UseAppEventBus hook

Important Notes

  • Server-side only: Application Events are internal to your server and don't communicate over the network

  • Asynchronous execution: Listeners execute independently and asynchronously

  • No execution order guarantee: The order in which multiple listeners execute is not guaranteed

  • Error isolation: Errors in one listener don't affect the emitter or other listeners

  • Error handling: ErrorMiddleware is not applied to app event listeners - you must handle errors within your listener methods

Hooks

Hooks in Socketio Decorator are functions that provides some services.

UseAppEventBus hook

The useAppEventBus hook allows you to use the application event bus dynamically in your code.

This hook provides the ApplicationEventBus instance that you can use to listen and emit events without using the @AppOn and @AppEmit decorators.

import { useAppEventBus, ApplicationEventBus } from "@admandev/socketio-decorator"

const appEventBus: ApplicationEventBus = useAppEventBus()

appEventBus.on({
    eventName: "new-message",
    targetClass: NotificationService,
    methodName: "sendMessage"
})

appEventBus.emit({
    eventName: "new-message",
    data: "Hello, world!"
})

ApplicationEventBus API

The ApplicationEventBus instance provides the following methods:

| Method | Description | |--------|---------| | on(listenerInfo: ListenerRegistration<TTarget>) | Registers a listener for a specific event | | emit(context: AppEventContext) | Emits an event to all registered listeners | | off(listenerInfo: ListenerRegistration<TTarget>) | Removes a specific listener for an event | | offAll(eventName: string) | Removes all listeners for a specific event | | removeAllListeners() | Removes all listeners |

The ListenerRegistration<TTarget> type is defined as follows:

| Property | Type | Description | |----------|------|-------------| | eventName | string | The name of the event to listen for | | targetClass | ClassConstructorType<unknown> | The target class constructor of the listener | | methodName | string | The name of the method to listen for in the target class |


UseIoServer hook

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()

UseUserSocket hook

The useUserSocket hook allows you to retrieve a specific connected socket instance based on a search argument (e.g., user ID).

  1. Setup the searchUserSocket function

    In 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
        },
    })
  2. Use the useUserSocket hook anywhere

    import { useUserSocket } from "@admandev/socketio-decorator"
    import { Socket } from "socket.io"
    
    const userSocket: Socket | null = await useUserSocket(userId)

UseRoomStore hook

The useRoomStore hook allows you to manage rooms dynamically in your code. It provides the RoomStore instance that you can use to set or get room data.

import { useRoomStore } from "@admandev/socketio-decorator"

const roomStore: RoomStore<ChatRoom> = useRoomStore()

roomStore.addRoom("myRoom", {
    id: "myRoom",
    messages: [],
    users: []
})

const room = roomStore.getRoom("myRoom")

RoomStore API

| Method | Description | |--------|---------| | addRoom(roomId: string, room: TRoom) | Adds a room to the store | | getRoom(roomId: string) | Gets a room by its id | | removeRoom(roomId: string) | Removes a room from the store | | clearAllRooms() | Clears all rooms from the store |


Example of using useRoomStore with room event handlers

class ChatRoomEvents {
    @OnRoomCreated("chat-*")
    public onChatRoomCreated(roomName: string) {
        console.log(`Room ${roomName} has been created`)

        const roomStore = useRoomStore<ChatRoom>()
        roomStore.addRoom(roomName, {
            id: roomName,
            members: [],
            users: [],
        })
    }

    @OnRoomJoined("chat-*")
    public onChatRoomJoined(roomName: string, socket: Socket) {
        console.log(`Socket ${socket.id} has joined room ${roomName}`)

        const roomStore = useRoomStore<ChatRoom>()
        const room = roomStore.getRoom(roomName)

        const socketDataStore = new SocketDataStore<DataStoreSchema>(socket)
        const member = socketDataStore.getData("member")
        
        if (room && member) {
            room.members.push(member)
        }
    }

    @OnRoomLeft("chat-*")
    public onChatRoomLeft(roomName: string, socket: Socket) {
        console.log(`Socket ${socket.id} has left room ${roomName}`)

        const roomStore = useRoomStore<ChatRoom>()
        const room = roomStore.getRoom(roomName)

        if (room) {
            const socketDataStore = new SocketDataStore<DataStoreSchema>(socket)
            const leftMember = socketDataStore.getData("member")

            room.members = room.members.filter(member => member !== leftMember)
        }
    }

    @OnRoomDeleted("chat-*")
    public onChatRoomDeleted(roomName: string) {
        console.log(`Room ${roomName} has been deleted`)

        const roomStore = useRoomStore<ChatRoom>()
        roomStore.removeRoom(roomName)
    }
}

UseRoom hook

The useRoom hook provides utilities for working with a specific room, including accessing room data and utility functions.

import { useRoom } from "@admandev/socketio-decorator"

// Get room data and utilities for a specific room
const { room, getClients, isEmpty, hasClientInRoom } = useRoom<ChatRoom>("myRoom")

Return value properties:

| Property | Type | Description | |----------|------|-------------| | room | TRoom \| null | The room data object if it exists, null otherwise | | getClients | () => string[] | Returns an array of socket IDs for all clients in the room | | isEmpty | () => boolean | Returns true if the room has no clients | | hasClientInRoom | (socketId: string) => boolean | Checks if a specific client is in the room |

[!NOTE] The utility functions are based on Socket.IO's room management (io.sockets.adapter.rooms) and may not reflect custom room data stored in RoomStore.

Dependency Injection

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.

🧪 Example project

Check out the full example using Express: 👉 Example on GitHub

🛠 Troubleshooting & Help

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