Controllers and routes
Create a controller
The Controller function creates a controller class bound to a root location path.
import { Controller, Get } from "@antelopejs/interface-api";
class HealthController extends Controller("/health") {
@Get("")
status() {
return { healthy: true };
}
}
Once your API provider is listening, request GET /health. Use @Get("") for the controller root, @Get(":id") for a relative path, or @Get() to use the method name. Import the controller from your module so the decorators register its routes.
For a first endpoint, continue with route handlers. Hierarchies, partial controllers, and programmatic registration are useful when the application grows; they are not required setup.
Controller hierarchy
Controllers support hierarchical nesting through the extend method. Each sub-controller inherits its parent's path prefix.
import { Controller } from "@antelopejs/interface-api";
// Handles routes at /api
class ApiController extends Controller("/api") {
// API methods
}
// Handles routes at /api/users
class UsersController extends ApiController.extend("users") {
// User-specific methods
}
Partial controllers
The PartialController function creates a controller that shares the same location as an existing controller. This is useful when you want to split routes across multiple files while sharing the same controller context and computed properties.
import { Controller, PartialController, Get } from "@antelopejs/interface-api";
// Original controller
class UsersController extends Controller("/users") {
@Get()
async listUsers() {
return { users: [] };
}
}
// Partial controller at the same /users path
class UsersAdminController extends PartialController(UsersController) {
@Get("admin")
async listAdminUsers() {
return { admins: [] };
}
}
Controller instances
Each HTTP request creates a new controller instance. The GetControllerInstance function retrieves a controller instance for the current request context, which is useful for reusing functionality across controllers.
import {
GetControllerInstance,
Controller,
Get,
RequestContext,
Context,
} from "@antelopejs/interface-api";
class UserController extends Controller("/users") {
async fetchUser(id: string) {
// Shared logic
return { id, name: "Example User" };
}
}
class OrderController extends Controller("/orders") {
@Get(":id")
async getOrder(@Context() context: RequestContext) {
// Reuse UserController logic within the current request
const userCtrl = await GetControllerInstance(UserController, context);
const user = await userCtrl.fetchUser("user-123");
return { order: { id: "order-1", user } };
}
}
The function ensures that computed properties and injected parameters are properly initialized before returning the instance.
Route handlers
Controllers contain methods decorated with HTTP method decorators that define route handlers.
An omitted location uses the method name: @Get() on listUsers registers /users/listUsers. Pass "" to register the controller root, or an explicit relative path such as ":id".
import {
Controller,
Get,
Post,
Delete,
HTTPResult,
} from "@antelopejs/interface-api";
class UsersController extends Controller("/users") {
@Get()
async listUsers() {
return { users: ["user1", "user2"] };
}
@Get(":id")
async getUser() {
return { id: "user123", name: "Sample User" };
}
@Post()
async createUser() {
return new HTTPResult(201, { id: "new-user-id", name: "New User" });
}
@Delete(":id")
async deleteUser() {
return new HTTPResult(204);
}
}
Handler modes
The API supports different handler modes that control when and how a method executes.
Choose a mode by what the handler needs to control:
| Mode | Use it to… | Effect of returning a value |
|---|---|---|
| Prefix | Reject or prepare a request before the main handler | Supplies the response and skips the main handler. |
| Main handler | Produce the endpoint's result | Supplies the response. |
| Postfix | Adjust a completed result | Stops subsequent postfix handlers. |
| Monitor | Observe completion, including errors | The return value is ignored. |
Within a mode, use handler priority to order matching handlers. Keep authentication or validation that must block the request out of monitors.
Prefix handlers
Prefix handlers run before the main handler. They are ideal for authentication, validation, and request preprocessing. If a prefix handler returns a value, that value becomes the response and the main handler is skipped.
import { Controller, Get, Prefix, HTTPResult } from "@antelopejs/interface-api";
class UsersController extends Controller("/users") {
@Prefix("get", ":id")
async validateUserExists() {
const userExists = true; // Check database
if (!userExists) {
return new HTTPResult(404, { error: "User not found" });
}
// Returning nothing allows execution to continue to the main handler
}
@Get(":id")
async getUser() {
return { id: "user123", name: "Sample User" };
}
}
Postfix handlers
Postfix handlers run after the main handler completes. They are useful for response modification, logging, and cleanup.
Warning: If a postfix handler returns a value, all subsequent postfix handlers are skipped.
import {
Controller,
Get,
Postfix,
Result,
HTTPResult,
} from "@antelopejs/interface-api";
class UsersController extends Controller("/users") {
@Get(":id")
async getUser() {
return { id: "user123", name: "Sample User" };
}
@Postfix("get", ":id")
async logUserAccess(@Result() result: HTTPResult) {
result.addHeader("X-Accessed-At", new Date().toISOString());
}
}
Monitor handlers
Monitor handlers run after request processing completes, regardless of success or failure. Their return value is ignored. Use them for logging, metrics, or other observation tasks.
import {
Controller,
Get,
Monitor,
Context,
RequestContext,
} from "@antelopejs/interface-api";
class UsersController extends Controller("/users") {
@Get(":id")
async getUser() {
return { id: "user123", name: "Sample User" };
}
@Monitor("get", ":id")
async logRequest(@Context() ctx: RequestContext) {
const status = ctx.response.getStatus();
const message = ctx.error ? String(ctx.error) : "ok";
console.log(`GET /users/:id -> ${status} (${message})`);
}
}
WebSocket handlers
WebSocket handlers manage persistent connections using the @WebsocketHandler decorator.
import {
Controller,
WebsocketHandler,
Connection,
} from "@antelopejs/interface-api";
class ChatController extends Controller("/chat") {
@WebsocketHandler("")
async handleChat(@Connection() connection: any) {
connection.on("message", (data: string) => {
connection.send("Echo: " + data);
});
connection.on("close", () => {
console.log("Connection closed");
});
}
}
Handler priority
Handlers can be assigned priorities to control execution order. This is especially useful when multiple prefix or postfix handlers match the same route.
import {
Controller,
Prefix,
HandlerPriority,
HTTPResult,
} from "@antelopejs/interface-api";
class SecuredController extends Controller("/api") {
@Prefix("get", "*", HandlerPriority.HIGHEST)
async checkAuthentication() {
const isAuthenticated = true;
if (!isAuthenticated) {
return new HTTPResult(401, { error: "Unauthorized" });
}
}
@Prefix("get", "*", HandlerPriority.HIGH)
async checkAuthorization() {
const isAuthorized = true;
if (!isAuthorized) {
return new HTTPResult(403, { error: "Forbidden" });
}
}
}
The available priority levels are:
| Priority | Value | Description |
|---|---|---|
HandlerPriority.HIGHEST | 0 | Executes first |
HandlerPriority.HIGH | 1 | High priority |
HandlerPriority.NORMAL | 2 | Default priority |
HandlerPriority.LOW | 3 | Low priority |
HandlerPriority.LOWEST | 4 | Executes last |
The Listen function
The Listen function starts listening on all configured servers.
import { Listen } from "@antelopejs/interface-api";
await Listen();
Programmatic route registration
This section is for integrations that create routes dynamically. Prefer decorators for routes that exist for the whole module lifetime.
Use RegisterRoute(handler) when a route's lifetime is shorter than its module's lifetime. It returns a numeric ID; retain that ID and pass it to UnregisterRoute(id) to remove the route. Module cleanup also removes owned routes.
import {
Controller,
RegisterRoute,
UnregisterRoute,
} from "@antelopejs/interface-api";
class HealthController extends Controller("/health") {}
const routeId = RegisterRoute({
mode: "handler",
method: "get",
location: "/health",
callback: () => ({ healthy: true }),
parameters: [],
properties: {},
proto: HealthController.prototype,
});
// When the route is no longer needed:
UnregisterRoute(routeId);
RouteHandler requires mode, method, the full location, callback, computed parameters, computed controller properties, and the controller prototype proto. priority is optional. RegisterRoute resolves module from the registering execution context; it does not use a caller-supplied owner.
Custom method decorators can use ProcessCallback(target, key, descriptor, mode, method, location?, priority?). It builds the full path and computed parameters from controller metadata and returns { id }. @Route(mode, method, location?, priority?) exposes the same operation as a decorator, including HTTP methods without a dedicated decorator.
Route introspection
Both introspection functions read the registry in the current interface instance without replacing the API provider:
| Function | Result |
|---|---|
getRegisteredRoutes() | Summary entries with a string id, location, method, callbackName, parameters, properties, optional priority, and optional owning module. |
getRegisteredRouteHandlers() | RegisteredRouteHandler[]: a string id and the complete handler, including its mode, callback, and prototype. |
Each call returns a new array, but nested metadata and handlers are shared references, not deep copies. Treat them as read-only. Entries disappear after explicit unregistration or owner cleanup. These functions describe registrations, not server readiness or a project-wide registry across all providers.