Django Ninja or FastAPI? What I'd Choose to Rebuild the Backend of an AI-Built App
Almost every Python backend an agent has written that reaches my desk is FastAPI. Not because a founder chose it: because Claude Code and Cursor reach for it when nobody says otherwise, and for a weekend prototype it is a fine reflex. Then the first customers arrive, a senior engineer joins or gets hired for a few months, and the first thing they say is «we should move this to Django». Then the founder writes to me asking who is right.
I write this from the seat that rebuilds these backends. Conectia takes apps built with Claude Code, Cursor, Lovable and friends to production, and when the verdict on the backend is rebuild, Python is the language we rebuild in more often than any other, because that is what the agent left behind and what the team can read. I have shipped both frameworks in production and I have no stake in either. What I have is a pattern: the framework question is a proxy for a different question, which modules failed first and what does each framework ship for exactly those modules.
The modules that failed are the ones FastAPI leaves to you
FastAPI is Starlette plus Pydantic: routing, request validation, dependency injection and an OpenAPI document for free. That is all it is, by design, and its author has never claimed otherwise. Everything else is yours to pick and wire: the ORM (SQLAlchemy 2.0 for most), migrations (Alembic), authentication, sessions, permissions, an admin, background jobs, a test harness. An experienced team assembles that in a week and is happy with the result.
An agent assembles it too, in minutes, and that is where the trouble starts. When I read an AI-built FastAPI backend, the failures line up with the eight symptoms I have described before, and they cluster on the parts FastAPI does not ship:
- Auth is a
get_current_userdependency that decodes a token and trusts it; refresh, revocation, password reset and email verification exist to the extent the agent was asked. - Tenancy is a
WHERE organization_id = …the agent remembered to add in most queries. The missing one is the data breach. - Migrations are absent or were generated once and never run again; the schema lives in whatever
create_all()produced on the first boot. - Permissions are
if user.role == "admin"scattered across routers. - The admin does not exist, so support happens in a SQL console against production.
None of these is FastAPI's fault. They are the modules a framework either ships or asks you to build, and an agent building them from scratch produces something that looks finished and is not.
Django ships the modules that broke, and Ninja removes the reason people avoided it
Django Ninja is a thin API layer on top of Django that takes its typing and validation from Pydantic. The request and response code looks like FastAPI: type hints, schemas, automatic OpenAPI, async views if you want them. Underneath you get Django, which means the things the agent improvised now come from a framework with twenty years of production behind it: the ORM, migrations that are part of the workflow rather than an afterthought, django.contrib.auth with password hashing, sessions and permission checks that have been audited for two decades, the admin, management commands and a test runner that knows about transactions and fixtures. Django 5.2 is a long-term support release, maintained until April 2028, which is a sentence a procurement team likes to read.
For the modules that broke, this is the difference between writing and configuring. Auth becomes a choice of backend and a settings block. Tenancy becomes a model manager that filters by organisation and a middleware that sets it, with a test that asserts every model carries the field. Permissions become declarations. The admin exists the day the models exist, and support stops touching production by hand. The reason people avoided Django for API products was the serializer ceremony of Django REST Framework; Ninja takes that away and keeps the rest.
I will concede the honest counter-argument. Django's async story is partial: the aget, asave and friends exist, but the database layer still hands the query to a synchronous driver in a thread. If your backend is twelve endpoints that call a model, stream tokens over SSE and hold websockets open, Django's weight buys you nothing and FastAPI's event loop buys you a lot. And if the FastAPI code the agent wrote works, carries no customer data and has no second tenant, rewriting it for brand reasons is the same mistake as the rewrite I argue against in the first post of this series. Framework choice is downstream of the verdict, never ahead of it.
The decision fits in one table
This is the rule I apply when the verdict on the backend module says rebuild.
| What the product needs | What I'd pick | Why |
|---|---|---|
| Users, organisations, roles, billing, an admin, audit trails | Django Ninja | Those are the modules that failed; Django ships them audited |
| A thin API over a model: inference, orchestration, streaming, websockets | FastAPI | Async end to end, small surface, nothing to configure that you do not use |
| Both: a business core and an AI layer | Django Ninja for the core, FastAPI for the AI service | Two processes, one Postgres, one contract between them |
| A working FastAPI backend with no tenancy problem and no customer data yet | Keep it | A rewrite without a failed module is runway spent on taste |
| A team that already runs SQLAlchemy and Alembic in production | FastAPI, with the missing modules bought, not written | Framework familiarity beats framework features when the team is senior |
The third row is the one I end up in most. The AI layer the agent wrote in FastAPI is often the best part of the codebase, because it is what the founder iterated on; the business core around it is what nobody read. Keeping the first and rebuilding the second on Django Ninja is cheaper than either extreme, and it fits the pattern I recommend for monoliths and services at MVP stage: one deployable per reason, not per fashion.
Rebuild in the order that keeps the product running
The rebuild that works is not a parallel universe where a new backend appears in three months and swaps in overnight. It is a sequence, and the sequence starts with what will not change.
- The database stays. Postgres is the asset. Point Django at the existing schema, bootstrap the models with
inspectdb, then clean them by hand until they say what the business means. The first migration is the one that records what already exists. - Migrations under version control from day one. Every schema change from here is a file in the repository that CI applies to a staging database before anyone deploys. This single habit removes symptom two, the database that gets slower every week, because an index becomes a reviewed change.
- Auth and tenancy before any endpoint. Pick the auth source (Django's own, or the external provider the app already uses, verified at the edge), write the organisation manager and the middleware, and write the test that fails when a model forgets the tenant field. Nothing else ports until this test is green.
- Freeze the contract with the old API. FastAPI and Ninja both emit OpenAPI. Snapshot the agent's document, and make every ported endpoint pass a contract test against it. The frontend does not learn the backend changed.
- Port by module, in the order of the go/no-go. Payments and anything that touches personal data first, because that is where the risk is; reporting and nice-to-haves last. Each module moves behind a route in the gateway, so the two backends run side by side for weeks.
- Give the AI layer its evaluation set. Whether it stays in FastAPI or moves, the model features get an eval set and a metric before they change. The suite is what makes the next prompt edit safe.
- CI, staging and a rollback you have rehearsed. Deploy the new core to staging on every merge, run the contract tests and the evals, and only then move production traffic one module at a time. The rollback is a route change, not a restore.
For a seed-stage product with a few thousand users, this is eight to ten weeks of one senior backend engineer with an architect looking over the design a few hours a week. Longer than a weekend, far shorter than the quarter a clean-room rewrite costs, and the product ships features the whole time.
What I'd do this month if the backend were mine
- Read the verdict before the framework. If nobody has named which modules failed, with the measurement next to each, that is the two-week job that comes first. Framework debates without a verdict are taste.
- List what the agent improvised. Auth, tenancy, migrations, permissions, admin, jobs. For each, write down whether it exists, whether it is tested, and whether a framework ships it.
- Decide per the table above, and write the decision down with the reason, so the next engineer does not reopen it.
- Keep the Postgres schema and the OpenAPI document as the two fixed points. Everything else can move; those two are the contract with your data and your frontend.
- Write the
CLAUDE.mdorAGENTS.mdbefore the first ported endpoint. The tenant manager, the rule against raw SQL, the migration workflow, the test command. The agent that wrote version one will write parts of version two, and it builds inside the conventions it can read. - Put a second person on the auth and tenancy review. Those two modules are where a rebuild either pays for itself or repeats the original mistake with better type hints.
The framework is the answer to a question you have to ask first
Django Ninja or FastAPI is a good question with a short answer once you know which modules failed and what your product owes its second customer. Ask it before the verdict and you get a religious war; ask it after and the table above settles it in a meeting.
That verdict is the first step of how we take an AI-built app to production, and when the backend has to be rebuilt it is the order above, run with you, with your own hire or with the agent that wrote the first version. If you want the before picture, I wrote down what breaks first in an AI-built MVP and how a Lovable app on Supabase differs, where the backend question has a different shape.



