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.
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)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.
# Works with Claude Code, Cursor, Copilot, Continue, Codex CLI
npx skills add lauren-framework/lauren-frameworkAuto-detects installed agents and copies skills to their global directory
Featured skills
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.
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
RustThe 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
TypeScriptModules, 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
PythonPydantic & 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.
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().
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.
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.
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.
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.
- 01Discover modulesWalk imports/exports, detect cycles.
- 02Compile providersResolve use_value / use_class / use_factory / use_existing.
- 03Build DI graphTopological order, scope-violation checks.
- 04Bake extractorsAuto-detect Path / Query / Json from annotations.
- 05Freeze radix routerStatic > param > wildcard, with Allow headers on 405.
- 06Wire pipelineMiddleware → guards → interceptors → handler.
- 07Lifecycle bootstrap@post_construct in topo order, with timeouts.
▌Elegant by design, powerful by nature
See how Lauren makes common patterns expressive and safe. Every line declares intent; the framework handles the rest.
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
Install
pip install lauren pydanticPython 3.11+, Pydantic v2, any ASGI server
Define
@controller + @moduleControllers with typed extractors, modules with explicit boundaries
Run
uvicorn app:app --reloadLaurenFactory.create() validates everything, then serves
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.
| Capability | FastAPI | Litestar | BlackSheep | Lauren |
|---|---|---|---|---|
| Routing model | Function-based, decorator-discovered | Class or function controllers | Class controllers | Class controllers, radix-tree, frozen at startup |
| DI scopes | One (request-scoped via Depends) | Hierarchical (per-context, cached by default) | Singleton / Scoped / Transient | Singleton / Request / Transient + scope-violation checks |
| Module system | APIRouter (no isolation) | Router / Controller hierarchy | None (app mounting only) | NestJS-style modules with imports / exports |
| Lifecycle hooks | Lifespan only | on_startup / on_shutdown | Startup / shutdown events | @post_construct / @pre_destruct in topological order |
| Protocol binding | Manual | Limited | Yes | Yes + multi-bindings + list[T] injection |
| Custom providers | Limited (dependency_overrides for tests) | Some | Yes (instance, factory, abstract bindings) | use_value / use_class / use_factory / use_existing |
| Subclass-decoration semantics | Implicit | Implicit | Implicit | Strict opt-in (MetadataInheritanceError) |
| Startup-time graph validation | Partial | Partial | Partial | Full — fails fast on cycles, scopes, ambiguity |
| Built-in error catalog | HTTPException only | Limited | Limited | 30+ error classes with stable codes |
| Structured JSON logging | BYO | BYO | Limited | ConsoleLogger / JsonLogger / NullLogger / InMemory |
| Graceful shutdown phases | Lifespan | Limited | Limited | 4-phase: drain → on_shutdown → @pre_destruct → goodbye |
| AI-ready docs (llms.txt) | No | No | No | Yes — bundled llms-full.txt |
| Auto-serialization of returns | Yes | Yes | Yes | Yes — dict, model, list, (body, status, headers) tuple |
| OpenAPI 3.1 generation | Yes | Yes | Yes | Yes |
| Module export violation detection | N/A | N/A | N/A | ModuleExportViolation at startup |
| WebSockets / SSE / Socket.IO | WebSockets | WebSockets + SSE | WebSockets | WebSockets + SSE + Socket.IO v5 |
Startup-time validation
Production logging
Graceful shutdown
Multi-team discipline
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 class | FastAPI | Litestar | BlackSheep | Lauren |
|---|---|---|---|---|
| Route-path conflict (same method+path) | First wins, no error raised | Varies by configuration | Errored | RouterConflictError at startup |
| DI cycle (A → B → A) | RecursionError at first request | Errored at first resolve | Errored at first resolve | CircularDependencyError at startup |
| Missing provider (UserRepo not registered) | First-request 500 | First-request 500 | First-request 500 | MissingProviderError at startup |
| Two providers for same Protocol | N/A (no Protocol binding) | Varies | Varies | ProtocolAmbiguityError at startup |
| Scope violation (singleton ← request) | N/A (single request scope) | Varies | Varies | DIScopeViolationError at startup |
| Module export violation | N/A | N/A | N/A | ModuleExportViolation at startup |
| Subclass accidentally registered as controller | Possible, silent | Possible, silent | Possible, silent | MetadataInheritanceError at startup |
Final scorecard
A subjective summary with the same trade-offs we'd give a colleague choosing a stack. 🟢🟢 Excellent 🟢 Good 🟡 Fair 🔴 Poor
| Criterion | FastAPI | Litestar | BlackSheep | Lauren |
|---|---|---|---|---|
| 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.
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)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
// 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
[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.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.