| title | Controllers and routes |
|---|---|
| description | Reference for controller classes, route and hook decorators, handler priority, and programmatic route registration of the AntelopeJS API interface. |
| mode | reference |
A controller groups endpoints under one path. A route is a controller method that returns a value. Hooks are for preparation or response handling that several routes share.
Controller(location) 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 the API implementation is listening, the controller answers GET /health. @Get("") registers the controller root, @Get(":id") a relative path, and @Get() the method name. The decorators register the routes when the module imports the file that declares the controller.
For a first endpoint, read route handlers. Hierarchies, partial controllers, and programmatic registration are optional.
A controller class has a static extend(location) method that returns a sub-controller. The sub-controller's location is the parent's location, a /, and the given location. It extends the parent class, so it inherits the parent's static members.
Controller(location) returns a ControllerClass, whose type parameter defaults to object. A bare ControllerClass instance therefore has no string index. To give the instances a type, pass a base class. Controller(location, Base) returns a ControllerClass<Base>, a controller class that extends Base.
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
}PartialController(controller) returns a controller class that extends controller and has the same location. Use it to split the routes of one controller across several files. The partial controller inherits the computed properties of the controller it extends.
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: [] };
}
}@antelopejs/api creates one instance of each controller class per HTTP request. GetControllerInstance(controllerClass, context) returns a promise of that instance for the current request, so one controller can call the methods of another. Its first argument is a ControllerClass<T>, a class built by Controller, PartialController, or extend, and the promise resolves to a T.
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 instance has its computed properties, the class properties that carry a parameter decorator, set before the promise resolves.
@Get, @Post, @Put, and @Delete turn a controller method into a route handler. Each takes an optional location and an optional mode (a RouteHandlerMode, default "handler").
An omitted location uses the method name, so @Get() on listUsers registers /users/listUsers. A symbol-keyed method has no name to use, so its decorator needs a location and throws a TypeError without one. Pass "" to register the controller root, or a relative path such as ":id". The full location joins the controller location and the route location, and repeated slashes collapse to one.
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);
}
}A handler mode sets when a method runs and what its return value does.
| Mode | Purpose | Effect of returning a value |
|---|---|---|
| Prefix | Reject or prepare a request before the main handler | A truthy value is the response. The main handler does not run. |
| Main handler | Produce the endpoint's result | Supplies the response. |
| Postfix | Adjust a completed result | A truthy value stops the later postfix handlers. |
| Monitor | Observe completion, including errors | The return value is ignored. |
Within a mode, handler priority orders the matching handlers. A monitor cannot block a request, so authentication and validation belong in prefix handlers.
@Prefix(method, location?, priority?) registers a handler that runs before the main handler, for authentication, validation, or request preparation. If a prefix handler returns a truthy value, that value is the response and the main handler does not run.
import { Controller, Get, Prefix, HTTPResult } from "@antelopejs/interface-api";
class UsersController extends Controller("/users") {
@Prefix("get", ":id")
async validateUserExists() {
const userExists = true;
if (!userExists) {
return new HTTPResult(404, { error: "User not found" });
}
// Returning nothing lets the main handler run.
}
@Get(":id")
async getUser() {
return { id: "user123", name: "Sample User" };
}
}@Postfix(method, location?, priority?) registers a handler that runs after the main handler, to change the response, log, or clean up.
Warning: If a postfix handler returns a truthy value, the later postfix handlers do not run.
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(method, location?, priority?) registers a handler that runs after request processing, whether it succeeded or failed. Its return value is ignored. Use monitors to log or to collect metrics. ctx.error holds the error thrown during processing, if any.
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})`);
}
}@WebsocketHandler(location?) registers a handler for WebSocket connections at the location. @Connection() injects the connection object of the implementation. The interface exports no type for it, so the example declares the methods it uses.
import {
Controller,
WebsocketHandler,
Connection,
} from "@antelopejs/interface-api";
/** The connection methods this handler uses. `@antelopejs/api` passes a `ws` WebSocket. */
interface ChatConnection {
on(event: "message", listener: (data: Buffer) => void): void;
on(event: "close", listener: () => void): void;
send(text: string): void;
}
class ChatController extends Controller("/chat") {
@WebsocketHandler("")
async handleChat(@Connection() connection: ChatConnection) {
connection.on("message", (data) => {
connection.send("Echo: " + data.toString());
});
connection.on("close", () => {
console.log("Connection closed");
});
}
}@Prefix, @Postfix, @Monitor, and @Route take an optional HandlerPriority. When several handlers of one mode match a route, the lower value runs first.
A prefix, postfix, or monitor handler matches its own location and every route below it. The location "" is the controller's own location, so the handlers below run for every GET route of SecuredController, such as /api/data. A * in a location is a literal path segment, not a wildcard.
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 | Second |
HandlerPriority.NORMAL |
2 | Default |
HandlerPriority.LOW |
3 | Fourth |
HandlerPriority.LOWEST |
4 | Executes last |
Listen() starts every configured server and returns a Promise<void>.
import { Listen } from "@antelopejs/interface-api";
await Listen();Decorators register routes that live as long as their module. RegisterRoute(handler) registers a route with a shorter life. It returns a numeric ID. Pass that ID to UnregisterRoute(id) to remove the route. When a module unloads, its routes are removed too.
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: {},
controller: HealthController,
});
// When the route is no longer needed:
UnregisterRoute(routeId);RouteHandler requires mode, method, the full location, callback, the computed parameters, the computed controller properties, and the controller class that declares the handler. priority is optional. A handler without controller makes RegisterRoute throw a TypeError (Cannot register GET /health: RouteHandler.controller is required). RegisterRoute registers a copy of the handler and sets its module to the module that calls it. A module value that the caller passes is replaced.
A custom method decorator can call ProcessCallback(target, key, descriptor, mode, method, location?, priority?). It builds the full location and the computed parameters from the controller metadata, registers the route, and returns { id }. It throws a TypeError when the class was not built by Controller. @Route(mode, method, location?, priority?) is the same operation as a decorator, and it covers HTTP methods that have no decorator of their own.
Both functions read the route registry of the current interface instance.
| Function | Result |
|---|---|
getRegisteredRoutes() |
Summary entries with a string id, location, method, callbackName ("anonymous" for an unnamed callback), parameters, properties, an optional priority, and an optional owning module. |
getRegisteredRouteHandlers() |
RegisteredRouteHandler[]. Each entry has a string id and the complete handler, with its mode, callback, and controller. |
Each call returns a new array, but the nested metadata and handlers are shared references, not deep copies. Treat them as read-only. An entry is removed when UnregisterRoute removes its route or when its module unloads. The functions list registrations. They do not report whether a server is listening.