OpenAPI

api mounts an OpenAPI 3.1 document as a first-class citizen: each operation dispatches to the controller method named after its operationId, request validation runs before your code does, and the document's security schemes become ordinary Punk guards - all resolved at boot.

my $api = api 'openapi.json';

An operation like

{
  "paths": {
    "/pets/{petId}": {
      "get": {
        "operationId": "getPet",
        "tags": ["pets"],
        "parameters": [
          { "name": "petId", "in": "path", "required": true,
            "schema": { "type": "integer" } }
        ]
      }
    }
  }
}

dispatches to MyApp::Controller::Pets::getPet (controllers resolve per tag), with petId already validated and coerced to an integer:

sub getPet {
    my ($c) = @_;
    my $id = $c->param('petId');        # validated params win the lookup
    ...
}

$c->openapi holds the whole validated parameter set - path, query, header and body - when you want it all at once.

Security as guards

my $api = api 'openapi.json' => {
    security => {
        key => sub {
            my ($c, $value) = @_;
            return $c->json({ error => 'nope' }, 401)
                unless valid_key($value);
            return;
        },
    },
};

Each security scheme in the document maps to a checker; a reference return short-circuits exactly like an under guard. Basic and Bearer credentials are extracted for you.

Scopes

Under a scope, an API mount inherits the scope's prefix and guards:

my $v1 = under '/v1' => $guard;
my $api = $v1->api('openapi.json');

The reference UI

docs '/api-docs';

Serves the generated documentation UI (Open::API::UI) for a mounted spec. With one api mount the mount is implied; name it when several are mounted. A docs path the spec already declares croaks at boot.

Starting from the spec

punk new MyApp --api ./openapi.json

generates the application with the spec mounted and a controller of operation stubs per tag. When the spec grows,

punk api sync

writes stubs for the operations you have not implemented yet.

Under the hood

Validation runs on Open::API's C ABI - schema checks compiled at boot, no per-request schema walk. A mounted operation serves at about 176,000 requests a second with validation on.