Controllers
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],
});
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Must match the JSON route file name |
controller | typeof BaseController | Yes | The controller class constructor |
prefix | string | No | URL prefix. Defaults to /{name} |
middlewares | (typeof BaseMiddleware)[] | No | Applied as Fastify preValidation hooks |
URL prefix rules
| Call | Route url | Final 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