Components

Controllers

Building HTTP request handlers with BaseController and JSON route files.

Controllers

Controllers handle HTTP requests via Fastify. Unlike services, controller methods are not auto-discovered — they must be referenced by name in a JSON route file.

Base class

import { BaseController, AppHandle } from "ioserver";

abstract class BaseController {
  protected appHandle: AppHandle;
  constructor(appHandle: AppHandle);
}

Creating a controller

import { BaseController, IOServerError } from "ioserver";
import { FastifyRequest, FastifyReply } from "fastify";

export class UserController extends BaseController {
  async getUsers(request: FastifyRequest, reply: FastifyReply): Promise<void> {
    const users = await this.appHandle.userManager.findAll();
    reply.send(users);
  }

  async getUser(request: FastifyRequest, reply: FastifyReply): Promise<void> {
    const { id } = request.params as { id: string };
    const user = await this.appHandle.userManager.findById(id);
    if (!user) throw new IOServerError("User not found", 404);
    reply.send(user);
  }

  async createUser(request: FastifyRequest, reply: FastifyReply): Promise<void> {
    const body = request.body as { name: string; email: string };
    const user = await this.appHandle.userManager.create(body);
    reply.code(201).send(user);
  }

  async deleteUser(request: FastifyRequest, reply: FastifyReply): Promise<void> {
    const { id } = request.params as { id: string };
    const deleted = await this.appHandle.userManager.delete(id);
    if (!deleted) throw new IOServerError("User not found", 404);
    reply.code(204).send();
  }
}

Route file

Create routes/user.json in your routes directory:

[
  { "method": "GET",    "url": "/",    "handler": "getUsers"   },
  { "method": "GET",    "url": "/:id", "handler": "getUser"    },
  { "method": "POST",   "url": "/",    "handler": "createUser" },
  { "method": "DELETE", "url": "/:id", "handler": "deleteUser" }
]

Registering a controller

// Default prefix: /user
server.addController({ name: "user", controller: UserController });

// Custom prefix: /api/v1
server.addController({ name: "user", controller: UserController, prefix: "/api/v1" });

// No prefix: routes at root
server.addController({ name: "user", controller: UserController, prefix: "" });

// With middleware
server.addController({
  name: "user",
  controller: UserController,
  middlewares: [AuthMiddleware],
});
OptionTypeRequiredDescription
namestringYesMust match the JSON route file name
controllertypeof BaseControllerYesThe controller class constructor
prefixstringNoURL prefix. Defaults to /{name}
middlewares(typeof BaseMiddleware)[]NoApplied as Fastify preValidation hooks

URL prefix rules

CallRoute urlFinal URL registered
addController({ name: "api" })/status/api/status
addController({ name: "api", prefix: "/v2" })/status/v2/status
addController({ name: "api", prefix: "" })/status/status
addController({ name: "api", prefix: "/v2" })//v2/

Lifecycle hooks

JSON route files can reference controller methods for any Fastify lifecycle hook:

[
  {
    "method": "GET",
    "url": "/admin/stats",
    "preHandler": "requireAdmin",
    "handler": "getAdminStats"
  }
]
export class AdminController extends BaseController {
  async requireAdmin(request: any, reply: any): Promise<void> {
    if (!request.user?.isAdmin) {
      throw new IOServerError("Admin access required", 403);
    }
  }

  async getAdminStats(request: any, reply: any): Promise<void> {
    reply.send(this.appHandle.statsManager.getStats());
  }
}

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

JSON Schema validation

Route files support full Fastify JSON schema on schema:

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

Error handling

Throw IOServerError for HTTP errors. Fastify's error handler serialises it to a structured response:

throw new IOServerError("Not found", 404);
// Response: { "statusCode": 404, "error": "IOServerError", "message": "Not found" }

For validation errors with schemas, Fastify returns 400 automatically before the handler is invoked.

Real-world example

The chat application defines two controllers with different prefix strategies:

ChatController at prefix "" (root):

// routes/chat.json
[
  { "method": "GET", "url": "/",       "handler": "getIndex"  },
  { "method": "GET", "url": "/health", "handler": "getHealth" }
]
server.addController({ name: "chat", controller: ChatController, prefix: "" });
// → GET /          → getIndex
// → GET /health    → getHealth

ApiController at default prefix:

// routes/api.json
[
  { "method": "GET", "url": "/status", "handler": "getStatus" },
  { "method": "GET", "url": "/stats",  "handler": "getStats"  }
]
server.addController({ name: "api", controller: ApiController });
// → GET /api/status  → getStatus
// → GET /api/stats   → getStats
Copyright © 2026