Routing
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.
| Registration | Route url | Final 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 |
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:
- Does not start with
_ - 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/chataddService({ 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);