Request Lifecycle¶
Introduction¶
When using any tool in the "real world", you feel more confident if you understand how that tool works. Application development is no different. When you understand how your framework functions, the tools feel less "magical" and you'll be more confident building your applications.
The goal of this page is to give you a good, high-level overview of how the Arvel framework boots and serves requests. The Application is the heart of it all — it owns the service container, runs your service providers, and produces the ASGI application your server runs.
Lifecycle Overview¶
First Steps¶
The entry point for an Arvel application is public/asgi.py. It calls create_application() from bootstrap/app.py and turns the result into an ASGI app:
# public/asgi.py
from bootstrap.app import create_application
asgi = create_application().into_asgi()
create_application() assembles the Application from its parts. You serve public.asgi:asgi with an ASGI server like uvicorn.
Building the Application¶
You rarely instantiate Application directly. Instead, bootstrap/app.py builds it through Application.configure(...), which returns an ApplicationBuilder you chain to declare config, providers, and routes:
from pathlib import Path
from arvel import Application
_BASE_PATH = Path(__file__).resolve().parent.parent
def create_application() -> Application:
routes_dir = _BASE_PATH / "routes"
return (
Application.configure(_BASE_PATH)
.with_config_dir(_BASE_PATH / "config")
.with_providers(_BASE_PATH / "bootstrap" / "providers.py")
.with_routing(
web=routes_dir / "web.py",
api=routes_dir / "api.py",
console=routes_dir / "console.py",
)
.create()
)
The Register and Boot Phases¶
Service providers boot the framework in two distinct phases. Understanding the difference between them is the key to wiring services correctly:
- register is synchronous and runs during
create(). Providers bind things into the container here — nothing more. Don't do I/O or resolve another provider's services; they may not be bound yet. - boot is asynchronous and runs after every provider has registered. By the time
boot()runs, every binding exists, so it's safe to resolve and wire services together, attach listeners, open connections, and so on.
boot() runs explicitly like this (common in scripts and tests) or automatically through the ASGI lifespan when the app is served.
Producing the ASGI Application¶
Once built and booted, the application becomes an ASGI app via into_asgi():
into_asgi() returns a FastAPI application with the exception handler registered, your routes mounted, a health route added, and the middleware stack installed. The request scope middleware is always present; observability, context, deferred-task, and maintenance middleware are added when enabled.
The Application Builder¶
Application.configure(base_path) returns an ApplicationBuilder. Each builder method returns the builder, so calls chain.
Builder Methods¶
| Method | Purpose |
|---|---|
with_providers(list \| Path) | Your service providers, as a class list or a path to a module that exposes a providers list. |
with_environment(name) | Force the environment ("production", "testing", …). |
with_config_dir(path) | A directory of config/*.py modules, enabling dotted-key config() lookups. |
with_config_files([...]) | Register typed ArvelSettings classes explicitly. |
with_routing(web=, api=, console=) | Route module paths to load. At least one is required. |
create() | Build and return the Application. |
Note
There is no with_settings or with_middleware on the builder. Settings come from with_config_dir / with_config_files, and middleware is attached to routes and groups or added to the ASGI app.
What create() Does¶
create() runs a fixed sequence:
- Load
.envfrombase_path/.envvia python-dotenv, withoverride=False— variables already in the process environment win. - Load the config directory (or a config cache, if one exists) when
with_config_dirwas used. - Load the providers module when
with_providerswas given a path. - Load the
webandapiroute modules, executing their decorators. - Construct the
Application, resolve the full provider chain (framework baseline + yours), and run every provider's synchronousregister().
Warning
with_routing(console=...) records the path, but the console route module is not loaded during create() yet. Define scheduled tasks and console commands as classes for now — see Task Scheduling and the CLI reference.
Note
After create(), container bindings from register() are available, but the asynchronous boot() has not run. Call await app.boot() (scripts/tests) or let the ASGI lifespan do it (serving).
Environment Resolution¶
The application environment is resolved in this order:
- An explicit
with_environment(...)value. config("app.env"), if a config directory is loaded.- The
APP_ENVenvironment variable, lowercased (defaulting to"production").
Read it at runtime with app.environment(). A handful of framework safeguards key off the environment — for instance, destructive migration commands and the database seeder refuse to run when the app is in production. See Configuration.
Serving the Application¶
In development you typically serve the module-level ASGI app with uvicorn, or use the arvel serve command, which runs public.asgi:asgi:
To run programmatically without the uvicorn CLI, use the serve helper, which boots through the ASGI lifespan:
Shutdown¶
On shutdown, the application disconnects registered services and runs each provider's shutdown() in reverse registration order:
Under ASGI this fires automatically on lifespan shutdown.
Lifecycle Errors¶
| Exception | Raised when |
|---|---|
BootError | A provider's register() or boot() raises. |
ShutdownError | A provider's shutdown() raises. |
EnvironmentNotSetError | environment() or base_path() is called before the app is configured. |