Core Conceptsο
Status: π΄ DA REVISIONARE
This page explains the model behind genro-asgi: how a server relates to the applications it serves, how a request finds its handler, and the design principles that make the whole thing predictable. Read it once and the how-to guides will read like footnotes.
The server / application modelο
genro-asgi separates two roles cleanly:
A server owns the runtime: one uvicorn loop, the middleware chain, the request registry, lifespan, and the set of applications it serves.
An application owns behaviour: a tree of
@route-decorated methods that answer requests. An application knows nothing about ports or middleware.
BaseServer and AsgiServerο
BaseServer is the minimal substrate every server shares: the uvicorn loop, a
monitored thread pool for blocking work, the primary app, the secondary mounts,
lifespan, and the request registry.
AsgiServer is the shipped, batteries-included server. It is a composition of
capability mixins stacked over BaseServer in a single MRO β communication,
auth, session, middleware, plugins, storage, and tasks. Each mixin contributes a
feature, and each feature is turned on through a constructor keyword argument:
server = AsgiServer(
primary=App(),
auth={...}, # arms the auth middleware
middleware={...}, # arms other middleware
tasks=True, # arms the task backbone
plugins={...}, # arms OpenAPI / pydantic plugins
)
The mixins exist whether or not you configure them β you never subclass to add a capability, you pass config to feed it. This is what βobjects always exist, backends come from configβ means in practice.
Primary vs mountsο
A server always has exactly one primary application (mandatory) and a dictionary of secondary mounts keyed by URL prefix (possibly empty).
The primary answers
/and everything no mount claims.A secondary mount answers requests whose first path segment matches its mount name.
# Conceptual shape: a primary "shop" plus a secondary "api" mount.
# Requests to /api/... reach the api app; everything else reaches shop.
The one demux ruleο
There is a single rule for dispatching a request to an application, the same on every server:
First path segment β secondary mount if one exists with that name, otherwise the primary app.
Running with only a primary and no mounts is a usage of this rule, not a different mechanism. There is no second dispatch path to learn.
The automatic _server appο
Every server automatically mounts an internal application at /_server/. You do
not configure it into existence β it is always present. On a public server it is
full (login, monitoring, OpenAPI of the system endpoints, task management);
its endpoints live under /_server/... and never leak into your own appβs route
tree.
Routing with genro-routesο
Routing is delegated to genro-routes, a protocol-neutral routing library.
This is a deliberate choice: because the route tree does not know whether it is
being called over REST, OpenAPI, or MCP, the same tree can be exposed through
several transports. It is the reason OpenAPI and MCP applications reuse your
ordinary @route methods rather than duplicating them.
@routeο
Import the decorator from genro_routes:
from genro_routes import route
Key forms you will use across the guides:
@route()β publish a method as an endpoint; its name is the URL segment.@route(media_type="text/html")β respond as HTML instead of JSON.@route(auth_rule="admin")β protect the endpoint (see below).@route(channel_channels="mcp")β also expose the method as an MCP tool.@route(task="cleanup", task_every="1s")β register the method as a scheduled task.
auth_rule and default-denyο
A route carrying auth_rule="admin" is protected: the callerβs avatar must carry
the matching tag. Protection is default-deny β a protected route answers
403 even when no auth middleware is configured, so you never accidentally ship
an open endpoint that was meant to be closed. The authentication
guide covers the credential side.
Requests and responsesο
A
Requestobject wraps the incoming ASGI scope: query parameters bind to your handlerβs signature, and the body is available for parsing.A handlerβs return value determines the response: return a
dictfor JSON, a string withmedia_type="text/html"for HTML, or an explicitResponseobject for full control.For long or open-ended bodies, return a
StreamingResponseor anSseStream(see the streaming guide).
Both Request and Response are importable from genro_asgi.
Design principlesο
genro-asgi is a spec-first redesign; its principles are ratified in
SPECIFICATION.md. Three of them govern almost every API decision:
No globals β state lives in instances. There is no module-level server and no ambient request. A server is an object you build; its components reach each other through an explicit parent reference (an application holds
self.server, a request holdsself.application). Two servers in the same process are fully isolated.Config is data, not structure. You describe what you want with plain data (dicts, config-builder calls) and hand it to objects that already exist. You do not restructure code to switch a backend; you change the data.
Objects always exist; backends come from config. A capability is not an
X | Nonethat a flag flips on. The session subsystem, the auth subsystem, the task subsystem β they are always present. Configuration decides which backend they use. This removes a whole category of βis it enabled?β branching.
Two more shape how you extend the framework:
Routes are static from boot β the route tree is fixed when the server starts; routing is never used as a mutable registry.
Extension is by subclassing. You grow an application by subclassing the right base (
OpenApiApplication,McpApplication,SessionMixin, and so on), not by patching instances at runtime.
The request flowο
Putting it together, here is the path an HTTP request travels:
HTTP request
β
βΌ
βββββββββββ
β uvicorn β one loop, owned by the server
ββββββ¬βββββ
βΌ
ββββββββββββββ
β AsgiServer β the ASGI app is the server object itself
βββββββ¬βββββββ
βΌ
βββββββββββββββββββββββββ
β middleware chain β errors β (wellknown) β (logging) β
β (outer β inner) β (cors) β (session) β (auth)
βββββββββββββ¬βββββββββββββ
βΌ
ββββββββββββββββββββ
β demux on first β segment matches a mount? β that app
β path segment β otherwise β primary app
ββββββββββ¬βββββββββββ
βΌ
ββββββββββββββββββββββββββ
β @route handler(**params)β query/body bound to the signature, typed
βββββββββββββ¬βββββββββββββ
βΌ
ββββββββββββ
β Response β dict β JSON, str+html β HTML, stream β chunks
ββββββ¬ββββββ
βΌ
ASGI send ββββββββββββΊ HTTP response
The middleware are ordered by priority (lower number = more outer). The always-on error middleware wraps everything, which is why an unmatched path or a raised HTTP exception becomes a clean status code rather than a stack trace. See the middleware guide for the exact chain and how to arm each stage.
Where to go nextο
How-to guides β apply these concepts to concrete tasks.
Getting started β if you skipped the runnable hello-world, start there.
SPECIFICATION.mdβ the founding decision log, for the full rationale.