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:

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:

  1. 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.

  2. Lifespan, Background Tasks, and Streaming Edge Cases

    Starlette provides lifespan events and background task primitives; FastAPI layers its own dependency lifecycle (yield dependencies) 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?

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

  1. How many distinct Pydantic models represent variations of the same entity?
  2. How often do schema changes require more than three files to touch?
  3. Can a newcomer trace a request’s dependency graph without asking for help?
  4. Do you rely on undocumented internal behaviors or private attributes of FastAPI/Pydantic?
  5. How much logic lives inside validators or dependency providers that would be clearer in plain service classes?
  6. Do you have a contract (versioning) policy, or do docs simply mirror the current code?
  7. 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:

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.

Start Your Financial Journey Today

Take control of your financial future with visual planning and dynamic budgeting. Sign up now and start managing your budget with confidence!