NAME

PAGI::FastAPI - Asynchronous, Type-Safe Micro-Framework with Dependency Injection and OpenAPI & Swagger UI

VERSION

Version v0.0.5

SYNOPSIS

use v5.36;
use PAGI::FastAPI;
use PAGI::FastAPI::Depends qw(Depends);
use Types::Standard qw(Int Str);
use Future::AsyncAwait;

my $app = PAGI::FastAPI->new(
    title   => 'Store Microservice',
    version => '1.0.0',
);

# 1. Add CORS Support
$app->add_cors(
    allow_origins => ['https://example.com'],
    allow_methods => ['GET', 'POST'],
);

# 2. Add Authentication Middleware Hook
#    (hand-rolled here for illustration only, for ready-made schemes,
#    including proper 401 challenges, see PAGI::FastAPI::Security)
$app->add_middleware(async sub ($c, $next) {
    my $auth = $c->header('Authorization') // '';
    if ($auth ne 'Bearer secret_token') {
        $c->status(401);
        return { detail => 'Unauthorized' };
    }
    $c->stash->{user_id} = 42;
    return await $next->($c);
});

# 3. Register Lifespan Handlers
$app->on_startup(async sub {
    warn "Connecting to database connection pool...\n";
});

$app->on_shutdown(async sub {
    warn "Closing database connections...\n";
});

# 4. Declare Async Dependencies
my $get_db = async sub ($c) {
    return { db_name => 'production_db' };
};

my $get_current_user = async sub ($c) {
    my $token = $c->header('Authorization') // '';
    unless ($token eq 'Bearer secret_token') {
        $c->status(401);
        return { detail => 'Invalid credentials' };
    }
    return { user_id => 42, role => 'admin' };
};

# 5. Route using HashRef Dependency Map
$app->get('/profile',
    dependencies => {
        db   => $get_db,
        user => $get_current_user,
    },
    handler => async sub ($c) {
        my $db   = $c->stash->{db};
        my $user = $c->stash->{user};
        return { user => $user, db => $db->{db_name} };
    }
);

# 6. Route using Depends() Array Spec
$app->get('/admin',
    dependencies => [
        Depends($get_current_user, key => 'user'),
        async sub ($c) {
            if ($c->stash->{user}{role} ne 'admin') {
                $c->status(403);
                return { detail => 'Admin privileges required' };
            }
        }
    ],
    handler => async sub ($c) {
        return { message => 'Welcome to admin panel' };
    }
);

# 7. Non-blocking GET route with path parameter & query validation
$app->get('/items/{id}',
    query   => { limit => Int },
    handler => async sub ($c) {
        return {
            item_id => $c->param('id'),
            limit   => $c->param('limit'),
            status  => 'active',
        };
    }
);

# 8. Non-blocking POST route with JSON payload validation
$app->post('/items',
    body    => { name => Str, price => Int },
    handler => async sub ($c) {
        return {
            created => 1,
            name    => $c->body('name'),
            price   => $c->body('price'),
        };
    }
);

# 9. Authentication via the companion PAGI::FastAPI::Security distribution
#    (extraction only, you supply the verification logic)
#
#    use PAGI::FastAPI::Security::HTTPBearer;
#    my $bearer = PAGI::FastAPI::Security::HTTPBearer->new;
#    $app->get('/secure',
#        dependencies => [ $bearer->depends(key => 'token') ],
#        handler      => async sub ($c) {
#            return { token => $c->stash->{token} };
#        }
#    );
#
#    See L<PAGI::FastAPI::Security> for HTTP Basic, API Key
#    (header/query/cookie), and OAuth2 password-bearer schemes.

my $pagi_app = $app->to_app;

DESCRIPTION

PAGI::FastAPI is an asynchronous micro-framework for modern Perl (5.36+) inspired by Python's FastAPI.

It combines non-blocking async execution via Future::AsyncAwait on the PAGI (Perl Asynchronous Gateway Interface) specification with request parameter validation via Type::Tiny and automatic interactive documentation generation.

Key Features

  • PAGI Protocol Engine: Asynchronous and non-blocking natively, built for scalable web applications.

  • Automatic Type Validation: Request query parameters and JSON payloads are checked against Type::Tiny constraints before reaching route handlers.

  • Automatic Interactive Docs: Serves an interactive Swagger UI interface at /docs and machine-readable OpenAPI 3.1 JSON at /openapi.json.

  • HTTP 422 Interception: Automatically intercepts invalid or missing parameters and returns formatted JSON errors with an HTTP 422 Unprocessable Entity status code.

  • Pluggable Authentication: Authentication is implemented as ordinary dependencies and middleware, with no framework lock-in. For ready-made schemes (HTTP Bearer, HTTP Basic, API Key, OAuth2 password bearer), see the companion distribution PAGI::FastAPI::Security, see "AUTHENTICATION AND SECURITY" below.

METHODS

new(%options)

my $app = PAGI::FastAPI->new(
    title   => 'My API',
    version => '1.2.3',
);

Instantiates a new PAGI::FastAPI instance. Acceptable options:

  • title - (Optional) Title string for the application and OpenAPI specification. Default: 'PAGI::FastAPI Application'.

  • version - (Optional) Version string for the OpenAPI specification. Default: '$VERSION'.

get($path, %options)

post($path, %options)

put($path, %options)

patch($path, %options)

delete($path, %options)

Registers a route for the specified HTTP verb. Options include:

  • query - (Optional) HashRef mapping query string keys to Type::Tiny type constraints.

  • body - (Optional) HashRef mapping request body keys to Type::Tiny type constraints. The request body is parsed as JSON by default; if the Content-Type header is application/x-www-form-urlencoded, it is parsed as form-urlencoded data instead. Either way, the same type constraints and validation apply.

  • dependencies - (Optional) HashRef or ArrayRef of dependency code blocks or PAGI::FastAPI::Depends specs.

  • handler - (Required) An async sub ($c) code reference executing business logic. Receives a PAGI::FastAPI::Context instance.

on_startup($code_ref)

on_shutdown($code_ref)

on_event($event_type, $code_ref)

Registers async callbacks for PAGI Lifespan Protocol events ('startup' or 'shutdown').

add_middleware($code_ref)

Registers an asynchronous middleware with the application. Signature:

$app->add_middleware(async sub ($c, $next) {
    # Pre-processing...
    my $res = await $next->($c);
    # Post-processing...
    return $res;
});

add_cors(%options)

Enables Cross-Origin Resource Sharing (CORS) with automatic handling of OPTIONS preflight requests. Options include:

  • allow_origins - ArrayRef or string of allowed origins (default: ['*']).

  • allow_methods - ArrayRef or string of allowed HTTP methods (default: ['*']).

  • allow_headers - ArrayRef or string of allowed HTTP headers (default: ['*']).

  • allow_credentials - Boolean enabling credentials support (default: 0).

  • max_age - Preflight cache max age in seconds (default: 600).

to_app()

my $pagi_closure = $app->to_app;

Generates and returns an asynchronous code reference conforming to the PAGI protocol specification: sub ($scope, $receive, $send). This closure can be served directly via pagi-server.

AUTO-GENERATED ENDPOINTS

PAGI::FastAPI automatically registers the following system endpoints:

  • GET /docs - Serves an interactive Swagger UI web viewer in the browser.

  • GET /openapi.json - Serves the generated OpenAPI 3.1 schema.

ERROR HANDLING

When a request fails parameter validation (either query string or JSON/form-urlencoded body), PAGI::FastAPI short-circuits handler execution and returns HTTP 422 Unprocessable Entity with a JSON body:

{
    "detail": "Query param 'priority' invalid: Undef did not pass type constraint"
}

Unmatched routes return HTTP 404 Not Found with {"detail": "Not Found"}.

AUTHENTICATION AND SECURITY

PAGI::FastAPI has no authentication built in by design, auth needs vary too much between applications to standardise, so the framework gives you two general-purpose building blocks instead:

  • Middleware (add_middleware), runs for every request, good for a single global auth check.

  • Dependencies (the dependencies route option, or PAGI::FastAPI::Depends), runs per-route, good for auth that varies by endpoint (e.g. some routes public, some requiring a token, some requiring a specific role).

A dependency signals an auth failure the same way any other dependency signals failure: by calling $c->status($code) with a code >= 400 and returning a body HashRef, which short-circuits the route handler before it runs:

my $get_current_user = async sub ($c) {
    my $token = $c->header('Authorization') // '';
    unless ($token eq 'Bearer secret_token') {
        $c->status(401);
        return { detail => 'Invalid credentials' };
    }
    return { user_id => 42, role => 'admin' };
};

Note that a dependency must signal failure this way, not by dieing, dependency execution is not wrapped in an eval, so a dying dependency propagates as an uncaught exception instead of a clean HTTP response.

Ready-made schemes: PAGI::FastAPI::Security

Writing the token-extraction and 401/403-response boilerplate above by hand for every scheme gets repetitive. The companion distribution PAGI::FastAPI::Security provides ready-made, Depends()-compatible classes for the common HTTP authentication schemes, modelled on Python FastAPI's fastapi.security module:

Each scheme only extracts the credential, it deliberately does not verify it, so you aren't locked into one JWT library, password-hashing scheme, or identity provider. Pair it with your own verification as a second dependency:

use PAGI::FastAPI::Security::HTTPBearer;
use PAGI::FastAPI::Depends qw(Depends);

my $bearer = PAGI::FastAPI::Security::HTTPBearer->new;

$app->get('/items',
    dependencies => [
        $bearer->depends(key => 'token'),
        Depends(async sub ($c) {
            my $claims = eval { verify_jwt($c->stash->{token}) };
            unless ($claims) {
                $c->status(401);
                return { detail => 'Invalid or expired token' };
            }
            return $claims;
        }, key => 'claims'),
    ],
    handler => async sub ($c) {
        return { user_id => $c->stash->{claims}{sub} };
    },
);

Every scheme also accepts auto_error => 0, resolving to undef instead of short-circuiting, for routes that behave differently for authenticated vs. anonymous requests. See PAGI::FastAPI::Security for the full documentation and an end-to-end JWT-verification example.

SEE ALSO

AUTHOR

Mohammad Sajid Anwar, <mohammad.anwar at yahoo.com>

REPOSITORY

https://github.com/manwar/PAGI-FastAPI

BUGS

Please report any bugs or feature requests through the web interface at https://github.com/manwar/PAGI-FastAPI/issues. I will be notified and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc PAGI::FastAPI

You can also look for information at:

LICENSE AND COPYRIGHT

Copyright (C) 2026 Mohammad Sajid Anwar.

This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License (2.0). You may obtain a copy of the full license at:

http://www.perlfoundation.org/artistic_license_2_0