Concepts

Routing

HTTP routing with JSON files and WebSocket routing with auto-discovery.

Routing

IOServer uses two distinct routing mechanisms — one for HTTP and one for WebSocket — each designed to keep configuration separate from logic.

HTTP routing (Controllers)

Route files

Each controller has exactly one JSON route file. The file must be named {controllerName}.json and placed inside the routes directory configured in IOServerOptions.routes (defaults to ./routes relative to process.cwd()).

// routes/api.json
[
  { "method": "GET",    "url": "/status",  "handler": "getStatus"  },
  { "method": "GET",    "url": "/users",   "handler": "getUsers"   },
  { "method": "POST",   "url": "/users",   "handler": "createUser" },
  { "method": "DELETE", "url": "/users/:id", "handler": "deleteUser" }
]

The handler value is a string matching the controller method name. IOServer binds it to the controller instance at registration time.

URL prefix logic

The final route URL depends on whether prefix is passed to addController.

RegistrationRoute urlFinal URL
addController({ name: "api" }) (no prefix)/status/api/status
addController({ name: "api", prefix: "/v1" })/status/v1/status
addController({ name: "chat", prefix: "" })//
addController({ name: "root" }) (no prefix)/anything/root/anything
An explicit prefix (even "") is used verbatim. Without prefix, IOServer defaults to /{controllerName} prepended to every route URL.

Lifecycle hooks in route files

Any Fastify route lifecycle hook can reference a controller method by string. The method is bound to the controller instance automatically:

[
  {
    "method": "GET",
    "url": "/protected",
    "preHandler": "requireAuth",
    "handler": "getProtectedData"
  }
]

Supported hook keys: onRequest, preParsing, preValidation, preHandler, preSerialization, onSend, onResponse, handler, errorHandler.

Schema validation

Fastify JSON schema validation works directly in route files:

[
  {
    "method": "POST",
    "url": "/users",
    "handler": "createUser",
    "schema": {
      "body": {
        "type": "object",
        "required": ["name", "email"],
        "properties": {
          "name":  { "type": "string", "minLength": 1 },
          "email": { "type": "string", "format": "email" }
        }
      },
      "response": {
        "201": {
          "type": "object",
          "properties": {
            "id":    { "type": "string" },
            "name":  { "type": "string" },
            "email": { "type": "string" }
          }
        }
      }
    }
  }
]

WebSocket routing (Services)

Auto-discovery

There are no route files for services. IOServer walks the prototype chain of a service class and registers socket.on(methodName, handler) for every method that:

  1. Does not start with _
  2. Is not constructor
class ChatService extends BaseService {
  // → socket.on("login", ...)
  async login(socket: any, data: any, callback?: Function): Promise<void> { ... }

  // → socket.on("send_message", ...)
  async send_message(socket: any, data: any, callback?: Function): Promise<void> { ... }

  // NOT registered — underscore prefix
  private _validateInput(data: any): boolean { ... }
}

Namespaces

Each service is registered on a Socket.IO namespace:

  • addService({ service: ChatService }) — namespace / (default)
  • addService({ name: "chat", service: ChatService }) — namespace /chat
  • addService({ name: "admin", service: AdminService }) — namespace /admin

The client must connect to the correct namespace:

// Default namespace
const socket = io("http://localhost:8080");

// Named namespace
const socket = io("http://localhost:8080/chat");

Client invocation patterns

Services support both acknowledgement callbacks and event-based responses:

With callback (recommended for request/response):

socket.emit("login", { username: "alice" }, (response) => {
  if (response.status === "success") {
    console.log("Logged in:", response.user);
  } else {
    console.error("Error:", response.message);
  }
});

Without callback (fire-and-forget or server push):

socket.emit("join_room", { room: "general" });
socket.on("room_joined", (data) => { ... });

Broadcasting

From inside any component, use appHandle.send() to push events to clients:

// Broadcast to all clients in a namespace
this.appHandle.send({ namespace: "chat", event: "announcement", data: { text: "..." } });

// Broadcast to a specific room
this.appHandle.send({ namespace: "chat", event: "new_message", room: "general", data: msg });

// Send to a specific client
this.appHandle.send({ namespace: "chat", event: "private_msg", sid: socket.id, data: msg });

From inside a service method, you can also use the socket directly:

// Broadcast to room (excluding sender)
socket.to("general").emit("new_message", message);

// Broadcast to room (including sender)
socket.nsp.to("general").emit("new_message", message);
Copyright © 2026