The Api layer

Modules declare JSON API endpoints through a declarative ApiConfig, one registration per module built into a registry as the module loads. The site calls Api::Route once, which serves every registered endpoint under /api.

How endpoints are registered

An API module declares its endpoints in an Endpoints function and calls it at load time from its index file. That single call registers the module's endpoints into the ApiConfig registry, so by the time the site runs, every included API module is already in the registry:

// at the bottom of QueueApi/index.php - registers at load
QueueApi::Endpoints();

Declaring a module's endpoints

Inside Endpoints, a module opens a version block with ApiConfig::Module and declares one endpoint per call with ApiConfig::Get or ApiConfig::Post (also Put and Delete). Within an endpoint you declare its inputs with ApiInputConfig, any guard with ApiConfig::Guard, and the handler with ApiConfig::Handler:

static function Endpoints(): void {
    ApiConfig::Module('Queue', 'v1', function() {

        ApiConfig::Get('list', function() {
            ApiConfig::Guard('QueueApi::Authenticate');
            ApiConfig::Handler('QueueApi::List');
        });

        ApiConfig::Post('process', function() {
            ApiInputConfig::String('queue');
            ApiConfig::Guard('QueueApi::Authenticate');
            ApiConfig::Handler('QueueApi::Process');
            ApiConfig::Error('QueueRateLimitError', 429, 'Queue rate limit exceeded');
        });
    });
}

An endpoint declared under module Queue, version v1, path process is served at /api/Queue/v1/process.

Serving the registry

The site calls Api::Route once in its render pass. It walks the whole registry and wires every registered endpoint under /api. Because each module registered itself at load, the site never lists the endpoints:

Api::Route();

Responses and errors

Responses have no envelope. On success the response body is exactly what the handler returns, JSON-encoded at the top level. There are no wrapper fields.

On error the HTTP status is the machine signal and the body is a short plain-text message. A handler can raise one directly with throw new ApiErrorException($status, $message). A module can also map a domain exception type to a status and message with ApiConfig::Error, as in the example above, so that raising that exception anywhere in the handler produces the mapped response.

Handlers are a place exceptions are caught

An Api handler is one of the few top-level places the framework catches exceptions, which shapes how a handler is written. The handler codes only the happy path: it reads its inputs, does the work, and returns the JSON it wants. When something is missing or invalid it simply throws, and the Api layer catches the exception and maps its class to an HTTP status and message centrally through ApiConfig::Error. There is no catch block in the handler itself.

This is why handler code carries few or no null checks and little defensive branching. Rather than checking for a missing record and returning an error shape by hand, the handler lets the lookup throw and trusts the central mapping to turn that exception into the right response. The result reads as a straight line of intent, and every endpoint reports errors the same way.

Next: Members and accounts.