It Is Time To Stop Recommending FastAPI
FastAPI changed the mood of Python web development. For a while the message felt almost irresistible: type hints, automatic docs, and async. Suddenly, your API looks âmodernâ before you've even written meaningful business logic. That honeymoon glow was deserved. The framework lowered the barrier to producing a respectable, documented HTTP interface. Teams that had previously labored through hand-rolled validation could ship something in an afternoon.
But the qualities that make a tool delightful on day one are not always the ones that sustain you on day 500. Over the past few years a pattern has emerged in larger FastAPI codebases: an initially frictionless path that slowly accumulates small architectural barnacles. Duplicated schemas, opaque dependency chains, internal mini-frameworks, and a growing sense that you're maintaining infrastructure nobody explicitly chose. This essay makes a modest proposal: FastAPI should remain a tool, not the default recommendation. We should get back into the habit of justifying the choice instead of inheriting it.
The Early Allure
FastAPIâs original pitch was: declare Pydantic models, annotate your function parameters, and you receive validation, parsing, interactive OpenAPI documentation, and a vaguely reassuring promise of performance because the word âFastâ is in the name â all of it offered âfor free.â The tutorials are friendly and kinetic; success comes quickly. In an ecosystem where Django can feel heavy and Flask unopinionated, FastAPI hit a sweet spot: just enough structure to help you, not so much that you feel trapped.
That story spread. Conference talks, blog posts, GitHub stars, copy-paste snippets. Before long, âWe'll spin up a FastAPI serviceâ became a sentence that sailed through architectural review with barely a raised eyebrow.
Where The Shine Dulls
Most of the long-term issues don't appear as dramatic failures. They arrive quietly.
The first is architectural unclarity. Tutorials push you toward a single file with decorators bound directly to the application object: @app.get("/items"). This is intoxicatingly simple, until you split features into modules. Suddenly you juggle routers, imports, and the minor dance required to avoid circular references. None of this is catastrophic; it is simply an accumulation of incidental complexity that a different design (standalone decorators plus a deliberate layering/registration model, as in Litestar) sidesteps altogether.
Documentation style is another subtle tax. FastAPI's public face is tutorial-first: narrative examples, approachable tone, incremental reveals. That works beautifully for learning but leaves gaps when you need a dry, exhaustive âwhat parameters can I really pass here?â reference. Engineers fall back to reading source or grepping for implementations. Over time that becomes institutional knowledge that new colleagues must rediscover.
Then there is the tight embrace with Pydantic. Again, initially a giving strong validation, clear error messages. But as soon as your canonical objects live somewhere else - SQLAlchemy models, dataclasses, attrs classes, msgspec structs - you begin duplicating field definitions. You create âreadâ models, âcreateâ models, âupdateâ models, public vs internal representations, and every new column or rename becomes a scavenger hunt. SQLModel tried to soften that, but its own maintenance cadence and feature coverage questions have made cautious teams wary. A framework like Litestar, which treats serialization as pluggable and can derive DTOs from a single source of truth, simply avoids most of that retyping.
Dependency injection tends to sprawl too. The decorator + Depends(...) style scatters construction logic across dozens of function signatures. You can do powerful things, certainly, but the price is a call graph whose shape is implicit. Testing then involves stacks of dependency overrides and fixtures. There are patterns for taming this, but they are not the ones you were shown on day one, and by the time you discover them, conventions may already be entrenched.
Async itself can be illusory. Codebases cheerfully mark endpoints async def while performing blocking database calls through sync drivers, executing CPUâbound transformations, or awaiting little of consequence. The event loop becomes theater; real performance characteristics are dictated by the database and serialization stacks. A âfast frameworkâ cannot compensate for architectural bottlenecks elsewhere, and the label sometimes lulls teams into postponing real instrumentation.
And perhaps most familiar: the slow emergence of a second internal framework. Someone introduces a base router. Someone else standardizes error envelopes. A colleague adds custom logging filters and structured tracing decorators. Before long there is a âcoreâ package responsible for compensating for one missing capability after another: problem detail shaping, layered guards, multi-registration of handlers under different auth policies, background task orchestration, model derivation. None of this is wrong. But teams rarely factor the build vs. adopt cost into the original decision. They conflate âwe can add it laterâ with âadding it later will be cheap.â
Governance & Maintainer Dynamics
It is healthy to separate technical critique from personal criticism. Public threads have, at times, surfaced concerns around:
- Bus factor & responsiveness: Historically, a large share of architectural direction and PR review rested with a small number of maintainers. Even as contributors broaden, latent fear of stalled triage can influence risk assessments for long-lived systems.
- Communication tone & docs curation: Decisions about documentation styleâemphasis on high-energy tutorials, liberal emoji use in earlier materialsâsparked debates about perceived seriousness. While stylistic, such choices shape adoption in larger or more conservative organizations.
- Issue & PR lifecycle transparency: Intermittent stretches of limited maintainer bandwidth created elongated waiting periods for reviewing deeper changes (e.g., advanced SQLModel PRs, Pydantic v2 adaptation proposals). This can nudge enterprises toward stacks with more distributed stewardship.
None of these observations indict the character or intentions of maintainers; they are reminders that governance posture is itself an architectural characteristic. When you choose a framework, you also choose its update tempo, its conflict resolution style, and its future adaptability. Teams should weigh that alongside feature checklists.
Starlette Dependency: Subtle Friction Points
FastAPI builds atop Starlette. In principle this is a strength: Starlette is a lean, well-designed ASGI toolkit. In practice, the layering can introduce specific issues:
-
Middleware & Exception Handling Interaction
Starlette's middleware stack order, combined with FastAPI's dependency-driven validation and exception translation, can yield surprising behavior: a Starlette middleware expecting to see raw request state may instead observe preprocessed objects; custom exception handlers may not trigger where developers expect because FastAPI intercepts and repackages certain errors earlier in the pipeline. Debugging these cross-layer interactions requires deeper knowledge of both frameworks' call chains, raising the cognitive load.
-
Lifespan, Background Tasks, and Streaming Edge Cases
Starlette provides lifespan events and background task primitives; FastAPI layers its own dependency lifecycle (
yielddependencies) and background task integration. Mixing them can surface non-obvious edge cases: double initialization, partial cleanup if an exception occurs during dependency finalization, or difficulties injecting error-aware teardown logic into streamed responses. Additionally, disconnect events for streaming clients are exposed at the Starlette layer, yet many FastAPI abstractions mask or complicate responding to them promptly.
These are solvable with discipline and instrumentation, but they are hidden costs. A framework more tightly integrated around a single layering model (or a deliberate decision to use Starlette alone) can reduce that ambiguity.
The Human Cost: Drift, Duplication, Diffusion
What does this feel like inside a growing codebase?
- You rename a field on a SQLAlchemy model and fix tests, only to discover two weeks later that a seldomâused PATCH endpoint still expects the old name in its Pydantic body.
- A new engineer spends hours tracing through layered dependency overrides whose purpose lives only in tribal memory because the original author left.
- You hesitate to upgrade Pydantic because you are unsure which thirdâparty extensions (and internal monkey patches) rely on undocumented behaviors.
- Your OpenAPI spec looks impressive, but you have no intentional versioning strategy, because schema evolution was emergent, not designed.
Each by itself is trivial. Together they form a low, steady drag on engineering velocity.
What Alternatives Offer
Different frameworks optimize for different balances of constraint and elasticity.
Litestar (built originally atop Starlette and now standing on its own) leans into layered composition: route handlers registered through standalone decorators, Routers and Controllers that carry shared configuration (dependencies, guards, DTO policies), and pluggable serialization with automatic DTO derivation from SQLAlchemy models, dataclasses, attrs, or msgspec structures. Instead of telling you âHere is a micro start and a separate bigâapp story,â it gives you one architectural grammar that scales from a single file to a multiâteam service without changing idioms midstream. It also bundles niceties that in FastAPI you would likely handâroll: problem detail responses, flexible caching âstores,â pluginâbased SQLAlchemy integration (plus the Advanced Alchemy layer with repositories and service abstractions), metrics emission, and event hooks. All of these are optional, but they are designed to interlock.
Raw Starlette remains a superb choice if you want to assemble everything consciously. You will write more glue, but you will understand every wire. Django, meanwhile, still dominates when you need an admin interface, form handling, mature ORM features married to a convention-rich ecosystem with decades of reference material and a hiring pipeline. Django plus Django REST Framework or Django Ninja provides a markedly different ergonomics profile: more ceremony up front and far less invention later.
The point is not that any of these is objectively âbetter.â It is that their failure modes and longâterm maintenance profiles are different. FastAPIâs initial ergonomics are compelling; its long-run ergonomics depend heavily on discipline you must supply yourself.
When FastAPI Still Makes Sense
If you are building a small internal service with a narrow lifespan; if your team already has mature internal scaffolding that neutralizes the pain points; if your domain model maps nearly 1:1 to public responses; and if Pydantic is already central to your data validation culture, then FastAPI can remain a pragmatic choice. Momentum matters; replacing a familiar stack has switching costs that must be justified.
But if several of the following resonate â multiple external schema variants (public vs partner vs internal), advanced SQLAlchemy usage, a desire to mix model types, a need for a structured event system, a push for vertical slice boundaries across teams, and frustration with duplicated DTOs â then it is responsible to at least prototype an alternative. Architecture debt is cheapest to avoid, not to refinance.
A Health Check For Existing FastAPI Codebases
- How many distinct Pydantic models represent variations of the same entity?
- How often do schema changes require more than three files to touch?
- Can a newcomer trace a requestâs dependency graph without asking for help?
- Do you rely on undocumented internal behaviors or private attributes of FastAPI/Pydantic?
- How much logic lives inside validators or dependency providers that would be clearer in plain service classes?
- Do you have a contract (versioning) policy, or do docs simply mirror the current code?
- What percentage of endpoint âperformanceâ issues in the last year were actually framework-related vs. database, network, or serialization?
If the honest answers are uncomfortable, improvement may require either a disciplined internal reform (codify patterns, extract domain logic, introduce DTO mapping) or a framework shift. Either is valid. Choosing intentionally is the victory.
Migrating Without Drama (If You Decide To Move)
A sensible path is evolutionary rather than revolutionary. Freeze and document your current external contract. Extract your domain logic into framework-agnostic modules (pure services and entities). Introduce a new slice (one feature area) in the target framework (Litestar, Starlette, or Django), wire it with metrics, tracing, auth, and persistence, then shadow traffic or run contract tests to build confidence. Only once you prove parity do you begin redirecting real requests. Most of the toil is in disentangling business logic from the old dependency injection lattice; that investment pays off regardless of where you land.
If You Stay: Reduce The Drag
You can markedly improve a FastAPI codebase without leaving it:
- Adopt vertical slice packaging (
features/users/...,features/widgets/...) instead of global âcontrollers/models/schemasâ silos. - Keep domain logic out of Pydantic validators. Limit those to shape and light coercion.
- Introduce explicit mapping layers between domain objects and external schemas; resist âmodel that does everythingâ temptation.
- Maintain an internal, parameter-complete reference for key decorators and utilities, even if generated automatically from signatures.
- Standardize error responses (e.g., Problem Details) early to avoid inconsistent ad hoc patterns.
- Instrument the app; replace assumed performance with measured latency breakdowns.
- Regularly prune unused dependencies, especially test-time overrides that add confusion.
These steps convert invisible friction into visible, governable structure.
A Cultural Shift: Re-Qualifying Defaults
The most powerful change a team can make is procedural: require a lightweight architectural decision record (ADR) for each new service, re-evaluated yearly. The ADR should state: domain characteristics, expected longevity, serialization needs, operational constraints, team familiarity, and the reasoning behind the framework choice. FastAPI may still win. But now it wins consciously, not by inertia.
Closing Thoughts
FastAPI earned its popularity; dismissing it outright would ignore the genuine advances it helped normalize in Python: type-driven validation, human-friendly auto docs, async ergonomics. Yet maturity in engineering is the willingness to notice when the default path quietly accumulates unpriced cost. Our job is not to defend earlier excitement; it is to optimize for clarity, maintainability, and adaptability over the real lifespan of the systems we build.
Keep FastAPI in the toolbox. Retire it as a reflex. Put alternatives on equal footing in the evaluation. Make decisions you can defend two years from now when a new engineer opens the repository and immediately understands why it looks the way it does.
That future engineer may be you.
If your team has a large FastAPI deployment that avoided these pitfalls, publish your structure and lessons. Shared experience sharpens everyoneâs judgment. If you are experimenting with Litestar or another approach, document the deltas honestly, including unexpected trade-offs. Hype got us here; candor will get us somewhere better.