Lauren vs FastAPI
The honest, technical comparison for teams choosing their Python backend stack.
“The fastest path from zero to demo”
Lineage: Starlette + Pydantic
Sweet spot: Solo apps, ML inference services, prototypes that ship fast
“Enterprise Python, built to survive the on-call rotation”
Lineage: Axum + NestJS + FastAPI
Sweet spot: Multi-team services; long-lived codebases; audit-heavy environments
Same endpoint, two approaches
A /users/{id} endpoint with a repository, a 404 envelope, and a logger. Same result — different mental model.
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)Key differences
Each row corresponds to a real source of bugs in production services.
| Capability | FastAPI | Lauren |
|---|---|---|
| Routing model | Function-based, decorator-discovered. Any file can register routes against the global app object. | Class controllers explicitly registered in @module(controllers=[...]). Visibility is enforced — unregistered controllers raise MissingProviderError. |
| Dependency injection | Single scope: one Depends() call per request. No scope enforcement. Singletons require a separate Container library. | 3 scopes: SINGLETON / REQUEST / TRANSIENT. Scope violations (singleton injecting request-scoped) raise DIScopeViolationError at startup. |
| Module system | None — routers included with app.include_router(). Any service can be imported from anywhere with no guardrails. | @module(imports=[...], exports=[...]) makes visibility explicit. Providers are reachable only if declared or transitively re-exported. |
| Startup validation | Partial — missing Depends, cycles, and ambiguous providers discovered on the first request. | Full 7-phase pipeline. Cycles, missing providers, scope violations, route conflicts all raise named StartupError subclasses before the app accepts traffic. |
| Error contract | HTTPException with a free-form detail field. Error shape varies per handler; no stable code string. | 28 documented error classes. Every error renders as {"error": {"code": "...", "message": "...", "detail": {...}}} — consistent across the entire framework. |
| Structured logging | Not included — you bring logging, structlog, or loguru yourself and wire it up. | Built-in ConsoleLogger (coloured) and JsonLogger (Splunk/Datadog). Per-request traces auto-leveled by status code. |
| Graceful shutdown | Lifespan protocol — a single on_startup / on_shutdown pair; no topological ordering or per-hook timeouts. | 4-phase: drain → on_shutdown callbacks → @pre_destruct in topological order → goodbye. Bounded timeouts at each step. |
| Inheritance semantics | Implicit — subclassing a decorated class can silently carry route metadata into the subclass. | Strict opt-in — MetadataInheritanceError raised at startup if a subclass registers without explicit re-decoration. |
When do bugs surface?
A startup error is a CI failure. A runtime error is a 3 a.m. page. Lauren catches every configuration bug before traffic flows.
| Bug class | FastAPI | Lauren |
|---|---|---|
| Route-path conflict (same method + path) | Last wins, silent | RouterConflictError at startup |
| DI cycle (A → B → A) | Hangs / recurses at first request | CircularDependencyError at startup |
| Missing provider (UserRepo not registered) | First-request 500 | MissingProviderError at startup |
| Two providers for same Protocol | First-request ambiguity | ProtocolAmbiguityError at startup |
| Scope violation (singleton ← request) | Stale-reference bug at runtime | DIScopeViolationError at startup |
| Subclass accidentally registered as controller | Possible, silent | MetadataInheritanceError at startup |
Final scorecard
A subjective summary — what we'd tell a colleague picking their stack.
| Criterion | FastAPI | Lauren |
|---|---|---|
| Time-to-first-endpoint | Excellent | Fair |
| Solo dev ergonomics | Excellent | Fair |
| 5-person team ergonomics | Good | Excellent |
| 50-person team ergonomics | Fair | Excellent |
| Type safety end-to-end | Good | Excellent |
| Startup-time validation | Fair | Excellent |
| Production logging out of the box | Poor | Excellent |
| Graceful shutdown semantics | Fair | Excellent |
| Stable error contract | Poor | Excellent |
| Multi-team module discipline | Poor | Excellent |
| Audit-friendliness | Fair | Excellent |
| Raw runtime performance | Good | Excellent |
| Ecosystem & docs (today) | Excellent | Fair |
Which should you pick?
Choose FastAPI when…
- Building a prototype, ML demo, or 50-line script where speed-to-first-request matters most
- Solo developer — the overhead of @module(providers=[...]) feels like noise
- Team doesn't need DI scopes, module boundaries, or multi-environment configuration validation
- Starlette ecosystem (plugins, middleware) is important to your project
- You want the largest Python web framework community and the most tutorials
Choose Lauren when…
- Multi-team backend service where providers must not leak between modules
- Long-lived codebase that will be handed to a rotating team over years
- Audit-heavy environment requiring stable error codes and structured JSON logs
- FastAPI project that has outgrown Depends() and needs real DI scopes
- Migrating from NestJS and want the identical mental model in Python
Ready to start?
Get your first Lauren service running in minutes — and experience the startup-validation difference first-hand.