Suppose we could redesign Catalyst without preserving every compatibility
decision it has accumulated. Which parts would we keep because they still
work? Which would we simplify? Which pain points have experienced Catalyst
developers learned to work around, but should not be inherited by a new
framework?
CatalystNext is the working name for that redesign. This is not a v1
proposal. I want to make the consequential choices visible while Catalyst
users can still challenge them: subroutine attributes or a DSL, chained
dispatch or another routing model, action and controller namespaces,
request-scoped state, services and MVC, and the boundary between a small core
and a complete framework.
PAGI, the Perl Asynchronous Gateway
Interface, would give CatalystNext an async-native execution foundation.
Supporting APIs, WebSockets, streams, MCP, and other current workloads
matters, but modernization is not the main question of this post. The main
question is whether the proposed programming model addresses Catalyst's real
strengths and pain points.
1. What should writing CatalystNext feel like?
Subroutine attributes, a route DSL, or conventions?
Catalyst is, for better or worse, a subroutine-attribute framework. An action
looks like a Perl subroutine with declarations attached to it. That keeps the
route metadata beside the code that handles the route, and it is an important
part of Catalyst's identity.
There are other credible choices.
A dedicated route DSL can show the whole application more clearly and avoid
some of Perl's attribute machinery. Rails demonstrates how much can follow
from a compact resource declaration:
resources :photos expands
into the conventional collection and member routes, named helpers, and
controller actions. At the other end, an explicit route table can make every
method, path, and handler visible without any convention.
FastAPI takes another
path: decorators remain close to the handler, while ordinary Python type
declarations also drive parsing, validation, serialization, and generated
OpenAPI documentation. The interesting part is not the Python syntax; it is
how much useful application metadata becomes available from one declaration.
My instinct is that CatalystNext should remain attribute-driven at its main
user-facing surface. I am not in love with inventing a large new DSL, and I
do not want a supposedly convenient route convention to conceal most of the
application.
Questions I would value feedback on:
- Are subroutine attributes still a pleasant and idiomatic way to describe actions in modern Perl?
- Is having route information beside the handler more valuable than seeing the complete route tree in one file?
- Which declarations should remain explicit even if a Rails-like resource shortcut is available?
- Would it be useful for attributes, a DSL, and conventions to be alternative front ends over the same underlying application model, or would that merely recreate Catalyst's “too many ways” problem?
The declaration surface does not settle what those declarations build. That
leads to the dispatch model itself.
What kind of dispatcher?
I am strongly inclined to keep chained dispatch. Chains remain a good way to
express the structure that real applications repeat:
identify tenant
→ authorize user
→ load project
→ load issue
→ perform endpoint
The problem is that Catalyst's historical chained syntax accumulated several
ways to say similar things. Controller namespaces, action namespaces, path
parts, captures, and private action names could all interact. People often
found chaining powerful only after they had survived learning how its pieces
were bolted together.
We are not bound by that compatibility surface in a new framework. The goal
would be to retain chains while separating three things that old Catalyst
often allowed to blur together:
- Code origin — the package and subroutine containing the code.
- Logical action name — the stable name used for chaining, diagnostics, and reverse lookup.
- URL path — the path used for HTTP dispatch.
Here is one deliberately incomplete syntax experiment:
package MyApp::Controller::AddressBook;
# URL: /contacts
sub contacts :Via('/') :At('contacts') {
# Work shared by the collection.
}
# URL: /contacts/{contact_id}
sub contact :Via('../contacts') :At('{contact_id:\d+}') {
# Work shared by one contact.
}
# Matches: GET /contacts
sub list :Via('../contacts') :Get('') {
}
# Matches: GET /contacts/{contact_id}
sub show :Via('../contact') :Get('') {
}
In this experiment, Via refers to a logical action and At contributes a
URL fragment. Get($path) is simply At($path) plus the HTTP GET method.
The same rule would apply to Post, Patch, and the other HTTP verbs.
The logical name of show is /contacts/contact/show. Its URL is
/contacts/{contact_id}. Its Perl package is
MyApp::Controller::AddressBook. Those names are related only because the
author chose them to be readable.
This is a strawman, not a finished grammar. In particular, relative action
references such as Via('../contact') need to remain understandable when an
application has several actions named contact. Ambiguity should be a boot
error, not a load-order rule.
Rails-style resource dispatch has an obvious advantage here: most developers
already know what index, show, create, update, and destroy mean.
Explicit route tables are also easier to explain than an action graph.
Chaining has to earn its additional conceptual machinery.
A second simplification may come from the execution model rather than the
route grammar. If the terminal action returns a response, each intermediate
action can be middleware around the rest of the chain: call the next action
and return its response, transform or replace that response, or return a
response immediately to short-circuit dispatch.
This would be CatalystNext middleware over the context and response values,
not raw PAGI middleware over event channels. It would give authorization,
resource loading, and response decoration one composable shape. The separate
question of whether action return values should acquire this meaning appears
below.
Questions:
- Do chains still model something important that middleware, dependency injection, or nested routers do not?
- Can a much smaller
ViaplusAtvocabulary make chained dispatch approachable? - Does treating intermediate chain actions as response-value middleware make the chain easier to understand?
- Should resourceful CRUD routes be a dispatcher of their own, a convenience that produces a chain, or not part of the core?
- Should one application be allowed to combine dispatch strategies, or does a framework need one blessed model?
Even after choosing chains, the framework must decide where the names in those
chains come from.
Should controller namespaces mean anything?
My current leaning is that a controller exists for code organization only.
Its package should tell the loader and the programmer where the code lives;
it should not silently contribute a URL prefix or an action namespace.
This will feel strange to people coming from frameworks where
Admin::UsersController naturally implies an /admin/users area. It also
removes an entire category of accidental coupling: reorganizing a large file
into several controllers would not change routes, reverse-route names, or
authorization behavior.
The action's logical namespace would instead come from its place in the
chain. A route dump would always show code origin, logical action, and URL
together, so the separation is visible rather than magical.
Questions:
- Is a purely organizational controller liberating, or does it discard a useful convention?
- When code origin and route structure differ, is a good route-inspection command sufficient to keep the application comprehensible?
- Should an application be able to opt into package-derived defaults, even if the framework's underlying model keeps the concepts separate?
The authoring model tells us how actions are named and reached. The next layer
is what enters, leaves, and flows between those actions.
2. What flows through the application?
Representations, content negotiation, and schemas
For a long time, content negotiation promised that one resource could produce
HTML, JSON, XML, or another representation based on the request. In practice,
many applications created an HTML site and a separately versioned JSON API.
JSON won a very large part of the API world, while server-rendered HTML has
continued to evolve rather than vanish.
Should one logical action still negotiate among representations? Or is
GET /contacts for HTML conceptually different from
GET /api/v1/contacts for JSON, even when both use the same underlying
service?
Schemas raise a related question. OpenAPI can document and generate clients
for HTTP APIs. MCP tool definitions use JSON Schema for inputs and can declare
schemas for structured outputs. A framework with a reliable model of action
inputs and results could potentially generate much of this rather than asking
the author to describe the same contract several times.
But “generate the schema” can mean several incompatible things:
- infer a best-effort schema from Perl code;
- require typed request and response declarations;
- start with a schema and generate Perl-facing types;
- treat schemas as optional metadata which extensions may consume.
Questions:
- Is content negotiation still a useful default, or should representations be explicit actions?
- How much type and schema information will Perl developers realistically maintain?
- Should validation, serialization, OpenAPI, and MCP schemas share one type model?
- At what point does schema-driven development stop feeling like Perl?
Schemas describe the public boundary of an action. Chained applications also
need a disciplined way to move values across their internal boundaries.
Request state, services, models, and views
Catalyst's stash was wonderfully flexible. That flexibility also made it easy
for action chains and views to communicate through undocumented hash keys.
I am considering a more structured replacement based on scoped attributes. A
controller could declare an attribute with request scope. Once a chain action
establishes its value, that value would be available to downstream
controllers and to a called view.
Conceptually:
has current_contact => (
scope => 'request',
isa => Contact,
);
That could make the data flowing through a chain explicit and introspectable.
It immediately creates hard questions:
- Is the value write-once, replaceable, or lazily built?
- Is it visible only downstream or everywhere in the request?
- Who performs async construction and cleanup?
- Does a view receive it automatically?
- Should a model or domain service ever see request-scoped state implicitly, or is that a layering violation?
There is a second naming problem here. “Model” has meant everything from a
database handle to a domain object to an external API client. “View” can mean
a template, serializer, response object, or negotiated representation.
Perhaps the framework should fundamentally understand services with
lifetimes—application, request, connection—and allow models and views to be
useful application-level categories layered on top. Or perhaps abandoning
strong MVC vocabulary would discard one of Catalyst's most helpful organizing
ideas.
I would particularly like examples from larger applications: where did
request-local state help, where did implicit availability cause trouble, and
which resources genuinely needed application, request, or connection scope?
These authoring and data-flow choices should address problems people have
actually encountered in Catalyst applications. They also cannot assume that
the applications we build today look exactly like the applications Catalyst
was originally designed to serve.
3. What else must the redesign support?
When Catalyst first appeared, the central use case was a database-backed,
server-rendered website: templates, forms, sessions, and HTML responses.
That remains important, but “web framework” now covers several workloads with
related but non-identical needs:
- server-rendered HTML, forms, redirects, and sessions;
- JSON APIs and webhooks;
- bidirectional WebSockets and server-sent event streams;
- large streaming request and response bodies;
- MCP tools, resources, and prompts;
- possibly work that outlives one HTTP request.
HTML did not disappear, but a new framework should not assume that every
request ends by selecting a template.
MCP makes the changed landscape especially visible. MCP's data layer is based
on JSON-RPC and exposes primitives such as tools, resources, and prompts.
Remote MCP commonly uses
Streamable HTTP,
but an MCP tool is not naturally an HTTP path. Its identity, input schema,
capabilities, progress notifications, and result types matter more than
inventing a REST-shaped URL for it.
Questions:
- Are HTTP, WebSocket, SSE, and MCP several protocol front doors into one application, or separate framework layers sharing services?
- Should MCP be a first-class CatalystNext concept or an add-on that consumes the same metadata as HTTP APIs?
- What request or connection lifecycle is shared across these protocols?
- Where should long-running work stop being a controller action and become a job or service?
These workloads do not need identical controller APIs, but they do need an
execution foundation capable of supporting all of them. That is where PAGI
enters the design.
4. PAGI as the foundation, not necessarily the user interface
PAGI applications receive a connection scope plus asynchronous receive and
send functions. This supports multiple incoming and outgoing events and
makes backpressure visible.
CatalystNext should take advantage of that foundation without forcing every
ordinary database lookup or HTML response to look like low-level protocol
code. “Async-native” needs to become a set of application-level guarantees:
- synchronous handlers remain straightforward;
- asynchronous handlers compose without blocking the server;
- cancellation reaches database queries, streams, and child work where possible;
- request and connection resources are reliably cleaned up;
- streaming does not require bypassing the framework;
- dropping to raw PAGI remains possible at a defined boundary.
The controversial part is how much of that contract belongs in CatalystNext
and how much belongs in PAGI-Tools, an event loop, or service implementations.
Should an action's return value mean something?
In Catalyst, an action's return value has no framework meaning. An action
changes the response attached to the context, and the dispatcher ignores
whatever the subroutine returns. The introductory documentation demonstrates
this by setting
$c->res->body
without returning a response.
That has confused people. Every Perl subroutine returns something, even
without an explicit return, yet return $something from a Catalyst action
has no effect on dispatch. The action's real result accumulates elsewhere on
the context.
There is a reasonable case for leaving this alone. A Catalyst action is a
stage in a request lifecycle, not necessarily a function which produces a
result. Several actions and a view can cooperate through the context, and an
accidental final expression cannot unexpectedly become the response.
But CatalystNext could make the terminal action's return value part of its
contract. It would have to return an object which isa PAGI::Response.
CatalystNext could supply a CatalystNext::Response subclass with framework
conveniences while accepting other PAGI response subclasses.
PAGI::Response is a detached
response value, so an action could be tested by inspecting its returned
status, headers, and body without a live connection. Response ownership,
redirects, errors, views, and chain short-circuits would all use the same
explicit value. The dispatcher could reject undef or an unrelated value
instead of guessing whether some other code completed the response.
The cost is a stricter and less forgiving endpoint contract. It may add
ceremony, fit awkwardly with views which build a response in several steps,
and turn Perl's harmless implicit return into a validation error.
Should CatalystNext give an action's return value framework meaning, or is
Catalyst's current behavior a useful consequence of its request lifecycle?
Even with a response-value contract for ordinary endpoints, some applications
will need direct ownership of the PAGI event channels. That is a separate
boundary.
How PAGI-shaped should an action be?
There is still a lower-level question underneath the response-value contract:
should ordinary actions see only the CatalystNext chain described above, or
should a terminal action be able to become a literal PAGI application and an
intermediate action become literal PAGI middleware?
PAGI::Nano provides a useful
precedent. A Nano application is an ordinary PAGI application, but most HTTP
handlers receive a friendly context and return a response value which Nano
sends as PAGI events. Middleware uses the PAGI-shaped
($scope, $receive, $send, $next) contract. A raw route can take ownership
of its response and reach the underlying channels when it needs them.
That gives Nano a “no silo, no cliff” property: application code does not pay
the raw protocol cost for an ordinary JSON response, but it never becomes
trapped above PAGI.
CatalystNext could take a similar approach with an explicit :Raw indicator
(the name is only a strawman). The routing attributes are omitted from this
example so we can focus on execution shape:
# An ordinary endpoint returns a CatalystNext::Response.
async sub show ($self, $c) {
return $self->view(show => contact => $self->current_contact);
}
# A raw intermediate action might receive the full middleware contract.
async sub observe :Raw (
$self, $scope, $receive, $send, $next
) {
my $observed_send = async sub ($event) {
$self->metrics->increment($event->{type});
await $send->($event);
};
await $next->($scope, $receive, $observed_send);
}
# A raw endpoint owns its response and can emit any PAGI event.
async sub feed :Raw ($self, $c) {
my $emit = $c->raw_send;
await $emit->({ type => 'contacts.feed.start' });
await $emit->({
type => 'contacts.updated',
contact_id => 42,
});
await $emit->({ type => 'contacts.feed.end' });
}
Here :Raw changes execution ownership, not routing. On a terminal action it
means that the action must complete the response or protocol exchange. On an
intermediate action it could mean that the action receives the full
around-middleware contract. That second shape can run before and after the rest
of the chain, replace or wrap receive and send, short-circuit dispatch, and
observe errors. It operates at a lower level than ordinary response-value
middleware, which sees the CatalystNext context, next, and the response
returned by the rest of the chain without taking ownership of the event
channels.
Keeping the PAGI event channels reachable also creates an extension seam
which response values alone cannot provide. A handler might emit a semantic
custom event such as contacts.updated.
A surrounding middleware could observe that event for metrics or tracing,
reject it, enrich it, or translate it into SSE, NDJSON, or some future wire
protocol. The handler can speak in application events while the wrapper owns
the representation. PAGI::Nano already
demonstrates this pattern
with custom send events rendered into more than one wire format.
Whether multiple wire formats should share one logical action is the
content-negotiation question discussed above.
The risk is making every CatalystNext action reason in transport-level terms.
Raw receive and send are flexible, but they also expose event ordering,
response ownership, awaiting every send, and protocol-specific failure modes.
The framework is supposed to design those footguns out of ordinary application
code.
So the question may not be whether CatalystNext is built from PAGI apps and
middleware—it almost certainly will be—but where that fact becomes visible:
- Should every endpoint and intermediate action directly implement a PAGI contract?
- Should ordinary actions receive a CatalystNext context and compile into PAGI
apps or middleware, with
:Rawas the explicit ownership escape hatch? - Can
:Rawsensibly mean terminal PAGI application on an endpoint and PAGI middleware on an intermediate action, or are those different concepts which deserve different indicators? - Should every ordinary intermediate chain action receive
nextand behave as response-value middleware, or should the framework also have simpler one-way setup stages? - Should custom event types be registered in the metamodel so extensions and tooling can discover them, or remain an intentionally open convention?
PAGI determines what CatalystNext can execute, but it does not determine the
right size of the framework or how much of Catalyst's existing shape a
redesign should preserve. Those are product decisions rather than protocol
decisions.
5. Where is the framework boundary?
How much compatibility should the name “Catalyst” imply?
I do not want to preserve awkward syntax solely because Catalyst had to remain
compatible with itself. A clean design needs permission to remove duplicate
spellings and historical namespace behavior.
At the same time, a framework called CatalystNext creates a reasonable
expectation that Catalyst developers can recognize its concepts and migrate
incrementally. PAGI can host existing PSGI applications through an adapter,
which creates possibilities for gradual migration at the application
boundary. It does not automatically provide source compatibility inside a
controller.
What matters most to potential users: familiar concepts, compatible
controller code, coexistence with an old application, migration tooling, or
simply a clear explanation of the break?
Even with permission to break old syntax, we still need to decide how much
belongs in the new core.
Small core or complete framework?
There is a perennial framework argument underneath all of this: should the
core be small and composable, or should a new application receive a coherent
answer for routing, configuration, services, validation, sessions,
authentication, rendering, errors, testing, and deployment?
A tiny core is easier to understand and less likely to freeze yesterday's
choices. It can also leave every application assembling a subtly incompatible
stack. A batteries-included framework creates shared vocabulary and better
out-of-the-box tooling, but every additional blessed subsystem raises the
cost of maintenance and makes alternative choices feel second-class.
My leaning is that CatalystNext should be small but complete and flexible.
That was one of Catalyst's strengths. It supplied enough shared structure for
an application to feel like one system, without insisting that every useful
idea had to live inside the core distribution.
“Complete” does not mean incorporating every fashionable application model.
It means defining a coherent application model, lifecycle, routing and
dispatch contract, service boundary, error path, and extension surface.
People should then be able to build more opinionated experiences on top:
Livewire- or LiveView-like stateful UI frameworks, API-focused stacks, MCP
tooling, or ideas none of us have named yet.
That still leaves a difficult boundary. Is schema generation a layer? Are
sessions? Are HTML views? Does authentication belong in the action model, in
middleware, or entirely in application code? How much must the core
standardize so independently developed extensions compose rather than merely
coexist?
I would like to hear which facilities must work together on day one for
CatalystNext to deserve the word “framework,” and which should remain
replaceable integrations.
Being both complete and extensible requires a shared structural vocabulary.
That is what keeps pulling me toward a metamodel rather than a collection of
unrelated hooks.
My current architectural leaning: build around a metamodel
The idea I keep returning to is a standalone application metamodel: something
structural and extensible, somewhat MOP-like in spirit but not based on Moose
and not limited to implementing CatalystNext.
It might contain concepts along these lines:
Meta::App
Meta::Controller
Meta::Action
Meta::Dispatcher
Meta::Service
Meta::View
Meta::Attribute
The exact class list is not the important part yet. The important separation
is between describing an application and turning that description into a
running PAGI app.
Subroutine attributes could build the model. A dedicated DSL or a
resource-routing convention could also build the model. Dispatchers could
compile actions into executable routing graphs. Extensions could inspect or
contribute metadata for OpenAPI, MCP, authorization, or developer tooling.
An application could fail at boot when routes collide or a chain consumes a
value nobody provides.
Spring WebFlux
is one useful precedent for separating these concerns: it supports both
annotated controllers and functional endpoint routing over the same reactive
web foundation. That does not mean CatalystNext should expose every possible
frontend. “They all compile to metadata” is an architectural capability, not
an excuse to avoid choosing one good default.
My hope is that the metamodel gives CatalystNext a sane, stable, introspectable
API for extension. Routes, actions, dispatchers, services, views, and scoped
attributes should be discoverable through documented objects rather than by
reverse-engineering controller packages or mutating framework internals.
Extensions should be able to contribute metadata, validate the application
model, and participate in compilation at defined points, while CatalystNext
still provides one opinionated default experience.
The metamodel also should not become a stealth decision about how application
objects are written. Ideally plain Perl, Moo, Moose, Object::Pad, and core
class code could participate through a structural contract. I am less sure
how far that neutrality can go before useful features become lowest-common-
denominator abstractions.
This is still only a direction. A metamodel can make routes inspectable and
extensions cleaner, but it can also become an abstract architecture project
that never produces a pleasant application framework. The user-facing
experience has to come first.
What feedback would help
I am not looking for a vote on whether every idea above is good. I would most
value reports from experience: what hurt, what scaled, and what you would not
want a redesign to lose.
- Which Catalyst behavior proved valuable in a large application, even if its syntax was confusing?
- Which Catalyst pain points have you learned to tolerate or work around?
- Which routing model do you find easiest to understand six months after writing it?
- Should an action's return value have framework meaning? If terminal actions
return a
PAGI::Response, does treating intermediate chain actions as middleware make chains clearer? - Where have framework conventions saved work, and where have they hidden too much?
- How have you successfully handled request-scoped values and async resource lifetimes?
- Would typed request and response declarations earn their cost through validation, OpenAPI, and MCP integration?
- What kinds of Perl web applications are you building—or would you build if the framework support existed?
- Where has direct access to an async protocol or custom event stream been useful, and where did it leak too much machinery into application code?
- Which facilities must be integrated before a framework is meaningfully more useful than a router and middleware collection?
- What would a Livewire- or LiveView-like layer need from the core in order to remain an extension rather than a fork?
- Should CatalystNext impose an object system, favor one without requiring it, or remain completely structurally typed?
- Does a reusable metamodel sound like a useful foundation, or like premature generalization?
Please feel free to answer one narrow part. A detailed objection to one
assumption is more useful at this stage than general encouragement.
I have spent enough time trying to imagine the perfect v1 in isolation. I
would rather find the shape of the problem with the community before turning
these experiments into promises.
Top comments (10)
That's a lot of questions..
I will just add a small comment: I moved away from Catalyst to basically plain Plack, Bread::Board and a Router class (i.e. OX, but sometimes I just use Router::Simple). One main reason was that I found it harder and harder to figure out which URI will touch which part(s) of the code, esp with
Chained.I now prefer to have single Router where all routes are listed and I can easily figure out where the corresponding code lives.
I also prefer to have my controller actions (and also the model methods) to be rather simple and explicitly call whatever helpers they need (eg for auth, formatting output etc). Together with middle-wares this allows (IMO) for rather clean and simple code without any / too much magic.
I do think that the idea of routing as part of the controller and not a god object is pretty heavily a part of Catalyst's identity and I get that approach doesn't fit everyone's mental model. The main advantage of the approach shows up in using controller roles and so forth. But when you have 200 endpoints it can feel ponderous. Which is fine because there's other ways to do things and for PAGI there's a toolkit that you can use to build something very similar to what you describe, if that works better for you. But routing needs to be cleaner (not 10 ways to do the same thing) and if you are building a simple application, like a dozen endpoints or less, being able to do that with one or maybe two files max should be possible.
I maintain several large sprawling Catalyst applications for a niche SaaS (~1,000 DAU), and have built many apps with Catalyst for almost two decades now. A few thoughts:
I don't see logging mentioned. In both Catalyst and Mojo apps, I swap out the default logger object with a compatible shim hooked to Log::Dispatch. I shuttle different traces to file logging, stdout/stderr, and graylog servers. Keeping things like Catalyst::Log extensible would be good.
Developer Experience right now is bad for junior devs, or experienced devs new to perl or Catalyst.
I have serious trouble onboarding devs to work on catalyst/DBIC. There are just so so many possible places where effects can happen, and they're not all easily discoverable. Here's a simple debug from one pageview of an app i'm working on today.
If a new dev is tasked with a change to one page, before they understand the code they may have to consider
How many different source files, directories, magic side effects, and data models need to go into the brain's working memory to understand this one pageview? And every page route is a unique snowflake when that question is asked.
This is the big pain point that's driving me away from Catalyst for new projects. Developer Onboarding. I like how Catalyst works and I understand it. It's overwhelming for newcomers.
Agree, you need to open too many files and grok unfamiliar patterns to follow just a single request. One thing that I love about some python frameworks I've played with is how the request/response domain models are often right in the same file as the controller. For simple things, which is 90% of what you do, Catalyst forces you to do the academically correct thing, like make a reusable model even if that model is only used in one controller.
And routing needs to be simple. Just one straight up approach.
This is an interesting design problem. Complexity makes a framework powerful, opinionated simplicity makes a framework fast and approachable.
Every framework I've used struggles with this balance over time. What set of features will:
Maybe LLMs are raising the bar on 2. Or lowering the bar. I'm not sure.
I'm looking to streamline Catalyst by removing the legacy feature and clarifying stuff that has 4 ways to work now, I think CatalystNext will remain a full fat framework with some elevated level of cognitive load compared to other systems. I think there's a role for that. Catalyst is never going to be a good fit for a small app with a dozen endpoints and one developer. Where is shines is when the system is complex and you have a lot of devs and the structure it gives has a lot of value.
I'll add some other things that I think are important:
Don't try to support ancient Perl versions. Aim for supporting Perl's released in the last 10 years, and use features of newer Perls (e.g. subroutine signatures).
Do not include custom versions of other libraries. As much as I like Mojolicious, it includes own forks of a lot of low-level modules. If there's a problem with the "standard" HTTP modules, work with the authors to fix them.
Don't be afraid to require other modules that provide functionality you need.
Don't be afraid to use cryptography modules for features that require them, such as signed cookies, CSPRNGs.
Don't gatekeep contributions. Host on Github or Gitlab or Codeforge.
Use secure defaults. Require uses to disable security features they don't want to use.
Include "community health" documentation: security policy, contribution policy, AI/LLM policy, code of conduct from the start.
PAGI itself requires Perl 5.18, I'd be likely aiming for that for core, although in practice you'd probably need at least 5.24 in order to use any of the more common async libraries. My goal is to keep the internal code to. 5.18 but in the examples and docs will use modern Perl syntax.
I wouldn't call it "CatalystNext" unless there's a path for migrating existing Catalyst applications and plugins.
I think the worship of MVC needs to go. Trying to keep them separate forces the application to get baroque and complicated.
I agree that routing should not be related to the organization of code.
I also think the framework should be agnostic about everything, and let the developers choose: router, logging, templates, database abstraction, configuration format. form handling, even PAGI vs Plack. (Yes, there would be some recommended defaults for beginners.)
I like something like Web::Machine, there the web application is a collection of "resources", and where necessary you can add handlers for low-level logic. Something updated for the modern web (with support for newer HTTP features such as preloading and CORS) with generic resource types, such as static pages.
MVC never really worked for the web and Catalyt never really came close to doing it anyway. With things like HTMLx and similar we can get closer but I think the goal is to move catalyst closer to 'easy thing are easy, hard things possible'. Right now Catalyst is good at making hard things possible but you have to be an expert, and it makes simple things a lot of hoops to jump through.
I also like Web::Machine and can still remember watching Steven present it when it first came out. I'm not 100% sure what part of that gets built into CatalystNext (which to be clear is a working title, although I do plan some examples of how to migrate).