[data-reveal]{opacity:1!important;transform:none!important}
Controllers and routes
API

Controllers and routes

Use a controller to group endpoints under one path. Start with a method that returns a value; add hooks only when several routes need the same preparation or response handling.

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:

ModeUse it to…Effect of returning a value
PrefixReject or prepare a request before the main handlerSupplies the response and skips the main handler.
Main handlerProduce the endpoint's resultSupplies the response.
PostfixAdjust a completed resultStops subsequent postfix handlers.
MonitorObserve completion, including errorsThe 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:

PriorityValueDescription
HandlerPriority.HIGHEST0Executes first
HandlerPriority.HIGH1High priority
HandlerPriority.NORMAL2Default priority
HandlerPriority.LOW3Low priority
HandlerPriority.LOWEST4Executes 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:

FunctionResult
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.