Perl MVC,
compiled
at boot.
it’s alive
Routes, guards, handlers and mounts are resolved once at
to_app and frozen. Nothing is interpreted per request:
dispatch is a hash lookup and your handler is a plain coderef call,
about two microseconds after the request arrived.
package MyApp; use Punk; get '/' => sub { $_[0]->text('oi!') }; get '/books/:id' => 'Web::Book#view'; post '/books' => 'Web::Book#create'; websocket '/chat' => 'WS::Chat#join'; sse '/events' => 'Live#feed'; 1;
The numbers
- requests / second, basic hello world app
- 200,000+
- per request, routed and answered
- 1.9µs
- Punk’s own share of that is around
- 0.5µs
- of the bare PSGI app’s throughput
- 99%
The whole application - routing table, guard chains, controller
resolution, template engines, database handles, OpenAPI validation - is
built once, when to_app is called. What runs per request is
C: an XS dispatcher walking frozen structures, calling your Perl exactly
once, for the handler.
- 2.01µs on a dynamic route with a capture - dynamic dispatch at static speed
- +0.3µs for a fifty-route table over a one-route table - table size is nearly free
- 2.06µs per Punk::Model::DBI get - the statement is memoised on the connection
- 3.2µs through a mounted OpenAPI operation, request validation included
Measured with bench/dispatch.pl in the
Punk distribution: the PSGI coderef called in a loop with a fresh
environment, no socket and no server, each app in its own interpreter,
best of nine rounds. A bare PSGI sub returning the same string costs
1.4µs on the same box, which is what the 0.5µs above is
measured against. The requests/second figure is
bench/bench.pl: the same apps hosted on two Hyperman
workers and driven with wrk -t4 -c64, median of three runs,
rounded - server and client share the box and it swings several percent
between runs, which is why the bare app measures the same 200,000 and
why the ratio is the honest half of that pair. All of it on one Apple
M5, and yours will differ; the ratios are the part that travels.
Everything is a keyword
use Punk exports a DSL that reads like a routing table and
compiles like a program. Each keyword below is resolved at boot; a typo'd
controller croaks before the app serves a single request.
-
Routing & guards
getpostputpatchdelany, with:nameand*restcaptures.underopens a nestable guard scope - return a response to short-circuit, nothing to continue. -
WebSockets
websocket '/chat' => 'WS::Chat#join'- routed like a GET, guarded like any route, then handed a live connection on the server’s event loop. Rooms broadcast; subprotocols negotiate. -
Server-Sent Events
sse '/events' => 'Live#feed'- the lighter sibling: an EventSource stream with heartbeats and retry control, fully non-blocking on a Hyperman worker. -
Sessions, CSRF, CORS
HMAC-SHA256 signed cookie sessions; single-use CSRF tokens that spend on use; CORS answered inside the dispatcher, so preflights never need a route and the headers reach the 404s too.
-
OpenAPI 3.1
api 'openapi.json'mounts the document: operations dispatch byoperationId, requests validate against the schema, security schemes become guards.docsserves the reference UI. -
Config & secrets
Layered YAML (
punk.yml→ env → local). Secrets are references -{ $env: KEY },{ $file: path },{ $exec: cmd }- resolved at boot and redacted everywhere else. -
Models, sync or async
One six-method contract -
get search all create update delete.Punk::Model::DBIblocks;Punk::Model::DBIx::Loopreturns futures and runs on the worker’s loop. Same SQL, same shapes. -
Outbound HTTP
$c->uais a Fetch agent - HTTP/2, futures, a keep-alive pool shared per worker. Named agents with their own timeouts and jars:ua partner => { timeout => 2 }. -
Mounts
staticfor files,markdownfor a docs site rendered and frozen at boot (this one),mountfor any PSGI app under a prefix. Longest prefix wins. -
Extensible by keyword
plugin,hook,middleware,helper- and a plugin can install keywords of its own withinstall_kw, indistinguishable from the core DSL. -
An all-C core
Thirty-one XS units behind the DSL: router, dispatcher, request, context, sessions, SSE frames, WebSocket codec. Speaks Hyperman and DBIx::Loop natively through their C ABIs.
-
Boot-time failure
Unresolvable controller? Unknown template engine? A docs path the spec already claims? The app refuses to start. What boots, serves.
Async without the ceremony
Return a future from any handler and Punk awaits it. On a Hyperman worker the request parks on the event loop and the worker keeps serving; on any other PSGI server the same code blocks politely instead. No colored functions, no framework fork.
$c->promise, $c->timer($secs) and
$c->await($f) are built in, in C, as
Punk::Future.
# Punk::Future: on a Hyperman worker the loop keeps serving # other requests while this one waits. get '/report/:year' => sub { my ($c) = @_; $c->model('report')->search({ year => $c->param('year') }) ->on_done(sub { $c->json($_[0]) }); }; # The async model backend: same contract as Punk::Model::DBI, # same SQL, but every method returns a future.
The spec is the router
Point api at an OpenAPI 3.1 document and the mount is
generated at boot: every operation dispatches to the controller method
named by its operationId, request bodies and parameters
are validated before your code runs, and the document’s security
schemes become ordinary Punk guards.
Start from the other end with
punk new MyApp --api ./openapi.json and the controllers
are stubbed for you, one per tag.
# Mount an OpenAPI 3.1 document: each operation dispatches to # the controller method named by its operationId, with request # validation and security-as-guards resolved at boot. my $api = api 'openapi.json'; docs '/api-docs'; # and the generated reference UI
docs '/api-docs'A queue in the box
Punk::Queue is a
job queue as a plugin: named queues, retries with backoff,
cron, locks, broadcast - and its keywords (queue,
task, cron) sit beside Punk’s own. Funky,
its admin UI, mounts under a guard you control.
use Punk::Plugin::Queue; plugin 'Queue' => { dsn => 'dbi:Pg:dbname=myapp', admin => { prefix => '/queue', guard => 'Web::Auth#admin' }, }; task 'mail.send' => 'Job::Mail#send'; cron '0 3 * * *' => 'Reports#nightly'; post '/signup' => sub { my ($c) = @_; $c->json({ job => $c->enqueue('mail.send' => [ $c->param('to') ]) }); };
The command line
punk new writes a running application - routes, a
controller, views, config, tests. Then punk dev serves it
with restart-on-change, punk routes prints the compiled
table, punk doctor reports the environment and C ABIs, and
punk config check resolves every secret before production
does.
punk routes
punk doctor
punk new boots intoBuilt on its own stack
Punk is the web tier of a set of zero-dependency Perl+C distributions that speak to each other through public C ABIs - no glue code, no foreign function overhead.
- Hyperman an event-loop PSGI server
- Fetch HTTP/2 Future-based user agent
- DBIx::Loop non-blocking DBI on your event loop
- Open::API OpenAPI 3.1 server and client
- Template::Stencil a fast template engine
- File::Raw::JSON fast JSON for File::Raw
Sixty seconds
cpanm Punk
punk new MyApp
cd MyApp && punk dev
Three commands to a running, tested application. Then read
Getting started, or see what
a finished one looks like - a live chat over WebSockets, with its own
docs site and OpenAPI reference, ships in Punk’s
example/ directory.