Metadata-First Python Web Framework

Enterprise Python, built to survive the on-call rotation

Inspired by Axum, FastAPI, and NestJS. Every route, DI binding, module boundary, and lifecycle hook is declared with decorators and resolved into an immutable execution graph at startup.

$pip install lauren
Python 3.11–3.14 ✓ASGI 3 ✓Pydantic v2 ✓mypy ✓OpenAPI 3.1 ✓
execution_graph.py
phase 1 / 7 · discovering modules
AppModuleAuthModuleUserServiceDatabaseJwtGuardUserRepoCacheClient
SINGLETON
REQUEST
TRANSIENT
python
from lauren import LaurenFactory, controller, get, module, Path

@controller("/hello")
class HelloController:
    @get("/{name}")
    async def greet(self, name: Path[str]) -> dict:
        return {"message": f"hello {name}"}

@module(controllers=[HelloController])
class AppModule:
    pass

app = LaurenFactory.create(AppModule)
30+
Error Classes
2000+
Tests
3
DI Scopes
7
Startup Phases
3.11–3.14
Python
Agentic Coding Ready

Give your AI agent full Lauren expertise in one command

60+ pre-loaded SKILL.md context packs cover auth, database, API patterns, observability, and architecture — instantly available in Claude Code, Cursor, Copilot, and every major coding agent.

bash
# Works with Claude Code, Cursor, Copilot, Continue, Codex CLI
npx skills add lauren-framework/lauren-framework

Auto-detects installed agents and copies skills to their global directory

Featured skills

PYDANTIC·UVICORN·HYPERCORN·GRANIAN·ASGI·OPENAPI 3.1·SOCKET.IO·ORJSON·MSGSPEC·PYDANTIC·UVICORN·HYPERCORN·GRANIAN·ASGI·OPENAPI 3.1·SOCKET.IO·ORJSON·MSGSPEC·PYDANTIC·UVICORN·HYPERCORN·GRANIAN·ASGI·OPENAPI 3.1·SOCKET.IO·ORJSON·MSGSPEC·
Features

25 flagship features, zero compromises

Each feature corresponds to a real source of bugs and outages in production Python services. Lauren catches them at startup, not at 3 a.m.

Radix-Tree Router

Routes compiled into a radix tree at startup. O(depth) lookup with static > param > wildcard priority. Route conflicts raise RouterConflictError at boot.

Three-Scope DI

SINGLETON, REQUEST, TRANSIENT scopes with compile-time violation checks. DIScopeViolationError fires before the first request, never during.

Protocol Binding & Multi-Injection

Bind providers to a Protocol, inject single or all via list[T]. Multiple providers without multi=True raise ProtocolAmbiguityError at startup.

Typed Extractors

Path[T], Query[T], Header[T], Cookie[T], Json[T], Form[T], Bytes, State, Depends[T], UploadFile — source auto-detected from type annotation.

Modules with Imports/Exports

Explicit dependency boundaries. Providers reachable only if declared or imported. Import cycles raise CircularModuleError at startup.

Lifecycle Hooks

@post_construct in topological order. @pre_destruct in reverse with bounded timeouts. Failures collected, never aborting teardown.

Guards & @set_metadata

can_activate(ctx) guards with ExecutionContext access. Parametrize via @set_metadata — one guard class handles every permission level.

Pipes

Post-extraction transforms declared inline via Annotated. DI-aware, chainable, sync or async. Convert raw values to domain objects in one step.

Interceptors

Wrap handlers after guards, before response is sent. Read route metadata, short-circuit for cache hits, or mutate results transparently.

Background Tasks

BackgroundTasks extractor — fire-and-forget after response. Sync tasks offloaded to thread pool. Participates in graceful shutdown drain.

File Uploads

UploadFile extractor for single or list[UploadFile] multi-file. Mix freely with Form[T] fields. Unicode filenames handled correctly.

Signals & SignalBus

Typed pub/sub bus. Listen to StartupComplete, RequestComplete, BackgroundTaskFailed and more. Errors in listeners are isolated, never crashing the bus.

Typed Streaming

Stream[T] inbound async iterator + StreamingResponse[T] outbound with automatic content-negotiation between SSE, NDJSON, and JSON Lines.

Auto-Serialization

Return dicts, Pydantic models, dataclasses, or (body, status, headers) tuples. Default encoder handles Enum, datetime, UUID, Decimal, set.

Onion Middleware

Three placement levels: global, per-controller, per-route. Class or function form. Same DI wiring as everything else.

Custom Exception Handlers

Class-form (DI-injected) or function-form handlers scoped globally, per-controller, or per-route. 30+ error classes with stable codes.

Custom Providers

NestJS-style recipes: use_value, use_class, use_factory, use_existing. Async factories awaited automatically. Environment-conditional swaps.

Sync Handlers

Regular def handlers auto-offloaded to a thread pool via anyio. The event loop is never blocked; async and sync handlers mix freely.

WebSockets & SSE

@ws_controller with typed @on_message. BroadcastGroup for room fan-out. EventStream with keep-alive and Last-Event-ID resumability.

Socket.IO v5

@socketio_controller namespaces with Engine.IO v4. Automatic ACKs, room join/leave, and full DI wiring identical to HTTP controllers.

Strict Decorator Inheritance

Subclasses are NOT automatically of the same role. You must opt in. MetadataInheritanceError catches silent misregistration at startup.

OpenAPI 3.1 Generation

Auto-generated from controller decorators and method signatures. Field descriptors emit constraints. @openapi_security annotations included.

Structured Logging

ConsoleLogger or JsonLogger (one-line JSON for log aggregators). Per-request traces auto-leveled: DEBUG 2xx, WARN 4xx, ERROR 5xx. NullLogger for tests.

Graceful Shutdown

Four-phase: drain → on_shutdown → @pre_destruct → goodbye. Bounded timeouts, idempotent re-entry, SIGTERM/SIGINT integration.

AI-Ready Documentation

Ships llms.txt and llms-full.txt. Paste into any coding agent for idiomatic Lauren on the first try.

Philosophy

Three frameworks walked into a bar

Lauren is what the bartender wrote down on a napkin afterwards. Borrowing the best ideas from three projects we admire deeply.

Axum

Rust

The Execution Graph

Everything needed to dispatch a request — routes, extractors, middleware — is a value composed at startup, not reflected on at request time. Lauren copies this insight: zero reflection on the hot path, predictable performance, and the ability to validate the entire app graph before accepting traffic.

NestJS

TypeScript

Modules, DI & Lifecycle

A full-fat IoC container with explicit module boundaries makes large codebases dramatically easier to navigate, test, and refactor. Lauren adopts @module(imports/exports), three DI scopes, Protocol binding, multi-bindings, and custom providers for the cases where @injectable is not enough.

FastAPI

Python

Pydantic & OpenAPI

Type hints are documentation. Pydantic v2 models for body validation, OpenAPI 3.1 from controller decorators and method signatures. Where Lauren diverges: types are resolved into a handler extraction plan at startup, not at request time. The runtime cost of validation is just validation.

01

Startup validates; runtime dispatches

If a graph error can be detected, it's detected before the app accepts traffic. Cycles, missing providers, scope violations, route conflicts — all caught in LaurenFactory.create().

02

Decorators attach metadata; they never rewrite functions

Every Lauren decorator sets a dunder attribute and returns the original object. No wrapping, no monkey-patching. Your handlers are still your handlers.

03

Type hints are introspection-ready, but only at startup

Lauren resolves type hints once, freezes the result, and uses it forever. Adding from __future__ import annotations is fine — ForwardRef resolution is handled centrally.

04

No global state

A LaurenApp owns a DIContainer. Multiple apps coexist in the same process — your tests rely on this. No module-level singletons, no implicit registries.

// startup

Seven phases. Then your runtime is pure traversal.

Every misuse — circular DI, missing module export, malformed extractor, conflicting routes — is rejected inside LaurenFactory.create(...), not on the first request.

  1. 01
    Discover modules
    Walk imports/exports, detect cycles.
  2. 02
    Compile providers
    Resolve use_value / use_class / use_factory / use_existing.
  3. 03
    Build DI graph
    Topological order, scope-violation checks.
  4. 04
    Bake extractors
    Auto-detect Path / Query / Json from annotations.
  5. 05
    Freeze radix router
    Static > param > wildcard, with Allow headers on 405.
  6. 06
    Wire pipeline
    Middleware → guards → interceptors → handler.
  7. 07
    Lifecycle bootstrap
    @post_construct in topo order, with timeouts.
~/proj $ uvicorn main:app --reloadlive
Code

Elegant by design, powerful by nature

See how Lauren makes common patterns expressive and safe. Every line declares intent; the framework handles the rest.

python
from pydantic import BaseModel
from lauren import (
    LaurenFactory, controller, get, post, module,
    Path, Json, injectable, Scope,
)
from lauren.exceptions import HTTPError


class NotFoundError(HTTPError):
    status_code = 404
    code = "not_found"


class CreateUser(BaseModel):
    name: str
    age: int

class UserOut(BaseModel):
    id: int
    name: str
    age: int


@injectable(scope=Scope.SINGLETON)
class UserRepository:
    def __init__(self) -> None:
        self._users: dict[int, dict] = {}
        self._next_id = 1

    def get(self, user_id: int) -> dict | None:
        return self._users.get(user_id)

    def create(self, name: str, age: int) -> dict:
        user = {"id": self._next_id, "name": name, "age": age}
        self._users[self._next_id] = user
        self._next_id += 1
        return user


@controller("/users", tags=["users"])
class UserController:
    def __init__(self, repo: UserRepository) -> None:
        self.repo = repo

    @get("/{id}")
    async def get_user(self, id: Path[int]) -> UserOut:
        user = self.repo.get(id)
        if user is None:
            raise NotFoundError("user not found", detail={"id": id})
        return UserOut(**user)

    @post("/")
    async def create(self, body: Json[CreateUser]) -> tuple[UserOut, int]:
        user = self.repo.create(body.name, body.age)
        return UserOut(**user), 201


@module(controllers=[UserController], providers=[UserRepository])
class AppModule:
    pass


app = LaurenFactory.create(AppModule)

Up and running in 5 minutes

1

Install

pip install lauren pydantic

Python 3.11+, Pydantic v2, any ASGI server

2

Define

@controller + @module

Controllers with typed extractors, modules with explicit boundaries

3

Run

uvicorn app:app --reload

LaurenFactory.create() validates everything, then serves

Comparison

How Lauren stacks up

FastAPI is the easiest path from zero to demo. Lauren is built for the enterprise long-tail — the codebase still around in five years, run by a rotating team, audited annually.

CapabilityFastAPILitestarBlackSheepLauren
Routing modelFunction-based, decorator-discoveredClass or function controllersClass controllersClass controllers, radix-tree, frozen at startup
DI scopesOne (request-scoped via Depends)Hierarchical (per-context, cached by default)Singleton / Scoped / TransientSingleton / Request / Transient + scope-violation checks
Module systemAPIRouter (no isolation)Router / Controller hierarchyNone (app mounting only)NestJS-style modules with imports / exports
Lifecycle hooksLifespan onlyon_startup / on_shutdownStartup / shutdown events@post_construct / @pre_destruct in topological order
Protocol bindingManualLimitedYesYes + multi-bindings + list[T] injection
Custom providersLimited (dependency_overrides for tests)SomeYes (instance, factory, abstract bindings)use_value / use_class / use_factory / use_existing
Subclass-decoration semanticsImplicitImplicitImplicitStrict opt-in (MetadataInheritanceError)
Startup-time graph validationPartialPartialPartialFull — fails fast on cycles, scopes, ambiguity
Built-in error catalogHTTPException onlyLimitedLimited30+ error classes with stable codes
Structured JSON loggingBYOBYOLimitedConsoleLogger / JsonLogger / NullLogger / InMemory
Graceful shutdown phasesLifespanLimitedLimited4-phase: drain → on_shutdown → @pre_destruct → goodbye
AI-ready docs (llms.txt)NoNoNoYes — bundled llms-full.txt
Auto-serialization of returnsYesYesYesYes — dict, model, list, (body, status, headers) tuple
OpenAPI 3.1 generationYesYesYesYes
Module export violation detectionN/AN/AN/AModuleExportViolation at startup
WebSockets / SSE / Socket.IOWebSocketsWebSockets + SSEWebSocketsWebSockets + SSE + Socket.IO v5

Startup-time validation

FastAPIPartial
LaurenFull — fails fast

Production logging

FastAPIBYO
LaurenBuilt-in

Graceful shutdown

FastAPILifespan only
Lauren4-phase

Multi-team discipline

FastAPINone
LaurenModule imports/exports

When do bugs surface?

A startup error is a CI failure. A runtime error is a 3 a.m. page. Lauren detects every graph error before accepting a single connection.

Bug classFastAPILitestarBlackSheepLauren
Route-path conflict (same method+path)First wins, no error raisedVaries by configurationErroredRouterConflictError at startup
DI cycle (A → B → A)RecursionError at first requestErrored at first resolveErrored at first resolveCircularDependencyError at startup
Missing provider (UserRepo not registered)First-request 500First-request 500First-request 500MissingProviderError at startup
Two providers for same ProtocolN/A (no Protocol binding)VariesVariesProtocolAmbiguityError at startup
Scope violation (singleton ← request)N/A (single request scope)VariesVariesDIScopeViolationError at startup
Module export violationN/AN/AN/AModuleExportViolation at startup
Subclass accidentally registered as controllerPossible, silentPossible, silentPossible, silentMetadataInheritanceError at startup

Final scorecard

A subjective summary with the same trade-offs we'd give a colleague choosing a stack. 🟢🟢 Excellent  🟢 Good  🟡 Fair  🔴 Poor

CriterionFastAPILitestarBlackSheepLauren
Time-to-hello-world🟢🟢🟢🟢🟡
Solo dev ergonomics🟢🟢🟢🟢🟡
5-person team ergonomics🟢🟢🟢🟢🟢
50-person team ergonomics🟡🟢🟢🟢🟢
Type safety end-to-end🟢🟢🟢🟢🟢🟢
Startup-time validation🟡🟢🟢🟢🟢
Production logging out of the box🔴🟡🟡🟢🟢
Graceful shutdown semantics🟡🟡🟢🟢🟢
Stable error contract🔴🟡🟡🟢🟢
Multi-team module discipline🔴🟡🟡🟢🟢
Audit-friendliness🟡🟢🟢🟢🟢
Raw runtime performance🟢🟢🟢🟢🟢🟢
Ecosystem & docs (today)🟢🟢🟢🟢🟡

Same endpoint, four frameworks

GET /users/{id} — DI-injected repo and logger, typed 404 envelope. Character counts are similar; the structural differences are what matter at scale.

python
from lauren import LaurenFactory, controller, get, module, injectable, Path
from lauren.exceptions import HTTPError
from lauren.logging import Logger

class UserNotFound(HTTPError):
    status_code = 404
    code = "user_not_found"

@injectable()
class UserRepo:
    def get(self, id: int): ...

@controller("/users")
class UserController:
    def __init__(self, repo: UserRepo, log: Logger) -> None:
        self.repo, self.log = repo, log

    @get("/{id}")
    async def show(self, id: Path[int]) -> dict:
        user = self.repo.get(id)
        if user is None:
            self.log.warn(f"user {id} not found")
            raise UserNotFound("user does not exist", detail={"id": id})
        return {"id": user.id, "name": user.name}

@module(controllers=[UserController], providers=[UserRepo])
class AppModule: ...

app = LaurenFactory.create(AppModule)
Enterprise

What “enterprise-ready” actually means

Most Python frameworks claim it. Lauren earns it through six hard guarantees. When the pager goes off at 3 a.m., the framework should be the boring part of the stack.

Validate-at-Startup

Seven-phase startup pipeline. Misconfigured DI graphs, route conflicts, circular modules, scope violations, and ambiguous Protocols all raise specific StartupError subclasses before the app accepts a single connection.

Module Boundaries for Multi-Team Codebases

NestJS-style @module(imports=[...], exports=[...]) makes every provider's visibility explicit. A provider is reachable iff declared here or transitively re-exported. No team can silently depend on another team's internals.

Deterministic Shutdown

Four-phase shutdown (drain → on_shutdown → @pre_destruct → goodbye) with bounded timeouts, idempotent re-entry, and full structured logging. Shutdown phases run in reverse topological order. SIGTERM/SIGINT integration is one function call.

Stable Error Contract

30+ documented error classes, every HTTP-mapped one with a stable code string. Every error renders as {"error": {"code": "...", "message": "...", "detail": {...}}} — same envelope across the entire framework, no per-handler shape repetition.

Structured Logging Out of the Box

ConsoleLogger (coloured, TTY-aware) or JsonLogger (one-line JSON for Splunk / Datadog). Per-request traces auto-leveled: DEBUG for 2xx/3xx, WARN for 4xx, ERROR for 5xx. NullLogger and InMemoryLogger for tests. Configure once, ship immediately.

Zero Runtime Reflection

No signature introspection or type-hint resolution on the dispatch path. Every route's extraction plan and every DI binding is resolved once at boot and frozen. Performance is predictable across cold and hot paths.

Stable error envelope — every time

json
// HTTP 404 Response
{
  "error": {
    "code": "user_not_found",
    "message": "user does not exist",
    "detail": { "id": 999 }
  }
}

// 30+ documented error codes, consistent envelope
// across the entire framework.

Startup log — every phase, every route

bash
[Lauren] INFO  [LaurenFactory]  Starting (root=AppModule)
[Lauren] INFO  [RouterExplorer] Mapped {GET /users/{id}} → UserController.show
[Lauren] INFO  [RouterExplorer] Mapped {POST /users} → UserController.create
[Lauren] INFO  [LaurenApp]      Application ready (1.2ms)  routes=12

[Lauren] WARN  [Request]  GET /users/999 404 2.1ms
[Lauren] INFO  [Request]  GET /users/1   200 0.8ms
[Lauren] ERROR [Request]  POST /users 500 3.4ms

[Lauren] INFO  [Shutdown] Drain complete.
[Lauren] INFO  [Shutdown] on_shutdown callbacks done.
[Lauren] INFO  [Shutdown] @pre_destruct hooks done.
[Lauren] INFO  [Shutdown] Goodbye.
Audience

Is Lauren right for you?

Lauren is the right choice if

  • Building a service that has to survive on-call rotations, where misconfigurations need to fail loudly and immediately
  • Working in a multi-team codebase where module boundaries and explicit exports matter more than a single-file demo
  • Migrating from NestJS and want the same mental model in Python
  • Coming from Rust + Axum and miss the compile-the-router-once approach
  • Already happy with FastAPI's Pydantic ergonomics but want stricter DI, modules, and lifecycle

Probably not if

  • Prototyping a 100-line script — use Flask or FastAPI instead
  • You want implicit, "just works" magic — Lauren is opinionated about explicitness
  • Your team doesn't believe in IoC — the boilerplate of @module(providers=[...]) will feel arbitrary
  • You need the largest ecosystem of third-party integrations today
  • Your project has fewer than 5 routes and a single developer

Ready to build something boring in the best way?

Join the community building enterprise Python services that survive the long haul.