August 26, 2026·7 min read

One Django app, one FastAPI edge, and where I draw the seam

I keep reaching for the same two-framework shape: Django for everything that owns the data, FastAPI for everything the outside world reads fast. The interesting decision isn't which framework — it's exactly where the line between them goes.

By Andrew Pyle

If you looked across the projects I run, you'd notice the same skeleton under most of them: a Django application and a FastAPI application, living in the same repository, serving the same product from two different angles. People sometimes read that as indecision — pick a framework! — but it's the opposite. It's a deliberate split, and the value is entirely in where the split falls.

Django and FastAPI are good at different things, and rather than force one of them to do the other's job badly, I let each do what it's best at and spend my thinking on the seam between them. Getting the seam right is most of the architecture. Getting it wrong is how you end up with two frameworks and the strengths of neither.

01Django owns truth

Django owns the truth

Everything that writes, everything that has to be correct, everything a human occasionally needs to reach in and fix — that lives in Django. The models, the migrations, the admin, the management commands that do the real mutating work. Django's whole gravity is around being a system of record: it gives you a schema you can evolve safely, an admin interface for free, and a mature story for the boring, load-bearing parts of owning data over years. When the question is "what is true and who is allowed to change it," the answer is on the Django side.

This is also where the guardrails live. The dry-run-and-revert commands, the data migrations, the imports and backfills — all of it is Django management commands, because that's where the authority to change the world sits. I want a single, auditable place where writes happen, with the framework's maturity behind it, rather than mutation scattered across whatever endpoint happened to be convenient.

Put every write behind one framework with a real admin and real migrations. Scatter your mutations and you've scattered your ability to trust them.

02FastAPI serves

FastAPI serves the world

The public, read-heavy surface — the API that a frontend or a crawler hits thousands of times, where latency and shape matter and nothing is being written — that's FastAPI. It's built for exactly this: fast, typed, async request handling with clean JSON out. The product's front door doesn't need the whole weight of the admin framework; it needs to answer read requests quickly and predictably, and to make the response shape a first-class thing rather than an afterthought.

The mental model I hold is that FastAPI is a projection of the truth Django owns, optimized for being read. It doesn't get to invent facts or change them; it reads the canonical data and serves it in the shape consumers want, fast. Keeping the read layer deliberately powerless — it can serve, it cannot write — is what makes it safe to expose widely and cache aggressively.

03One-way seam

The seam is a one-way street

The rule that keeps this from turning into a tangle is that the seam only flows one direction. Django is upstream; FastAPI is downstream. Truth is authored, migrated, and corrected on the Django side, and it flows down to the FastAPI side to be served. The read layer never reaches back to mutate the record. The instant you let the fast public API write to the system of record, you've reunited the two responsibilities you split on purpose, and now your front door — the most exposed, most cached, least trusted surface — can change the truth. That's the failure I design against.

Concretely, that means the two share a database that Django owns the schema of, and FastAPI reads from with read-shaped queries. Or, for the parts that can go stale gracefully, the truth gets baked into a snapshot at build time and FastAPI serves that. Either way the direction of authority is fixed and visible: change flows down the seam, never up.

The same discipline shows up in how I ship the thing at all. The config that runs the box lives upstream in the repo and flows down onto the server, never the other way around, and the deploy that installs it checks its own work and reverts on failure rather than trusting the change. Upstream authors, downstream serves — it's the identical rule whether the downstream artifact is an API response or a live nginx config. Once you've internalized the shape in one place, you start seeing everywhere it should hold.

04In the code

What the seam looks like in code

None of this is abstract in my repos — the seam is a specific handful of lines. The FastAPI process boots by calling Django's setup before it imports a single router, so it comes up inside Django's world: the same settings module, the same models, the same database connection Django owns. Then a read router imports the Django model directly and queries it. There is no second copy of the schema, no ORM mirror, no message bus in between. FastAPI borrows Django's models wholesale and only ever reads through them.

# fastapi_app/main.py — boot into Django's world first
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "andrew_j_pyle.settings")
django.setup()            # same settings, same models, same DB

# fastapi_app/routers/blog.py — then just read the canonical model
from blog.models import BlogPost

post = await sync_to_async(
    lambda: BlogPost.objects.prefetch_related("paragraphs").get(slug=slug)
)()

The one real wrinkle is that Django's ORM is synchronous and FastAPI's request path is async, so every read is wrapped to cross that boundary cleanly. The queries run through a sync-to-async shim, shaped for reading with prefetch and annotated counts so a page of essays is one trip to the database, not one plus N. That wrapper is doing more than plumbing. It's the physical form of the seam: FastAPI touches the data only through a narrow, read-shaped doorway into the schema Django defines, and there is deliberately no matching write doorway on that path. If someone wanted to add one, they'd have to notice they were building the thing the whole design exists to prevent.

05Baked projections

When the projection is a build artifact

Not every projection is a live database read. Some of the truth on my sites can go a little stale without anyone being hurt, and for those I push the projection one step further: I bake it into a file at build time and serve the file. The essays you are reading now work exactly this way. A build script fetches every essay from the FastAPI blog API and writes them into a single JSON snapshot, which the prerender step turns into real, crawlable HTML for each essay — so a search engine gets the full text and the right title, not an empty React spinner.

The reason to bake rather than read live is partly speed and partly crawlers, but the discipline around it is the same one-way rule. The build fetches from the read API — downstream of Django, never touching the record — and if that fetch fails it keeps the last committed snapshot instead of shipping a blank page or failing the build. That fail-to-the-last-good-copy instinct is the same one behind the homepage's build-time stats: fetch the truth at build, but never let a hiccup upstream blank out the page. The snapshot is a projection of a projection, and it is still strictly downstream — the file can be crawled and cached and served from the edge, and it can never write back up the seam to change what an essay says.

06Two beats one

Why two frameworks beats one compromise

You can absolutely build any of this with a single framework, and for a small enough project you should. The reason I keep reaching for the split is that the two jobs pull in opposite directions. A system of record wants maturity, safety, batteries-included admin, and a slow, careful attitude toward change. A public read API wants speed, minimalism, and to be dumb on purpose. Force one framework to be excellent at both and you spend your life fighting its defaults on one side or the other.

Splitting them lets each be unapologetically itself, and moves all the interesting design work to the seam — which is exactly where I want my attention, because the seam is where the real decisions are. Which facts are authoritative and which are projections. What can go stale and what must be live. Where authority ends and reading begins. Pick the frameworks in an afternoon; the seam is the part worth thinking hard about, and it's the part that decides whether the whole thing stays sane as it grows.

There's a compounding reason too: this same split is the skeleton under most of what I run, so the cost of understanding it is paid once and reused everywhere. When a new project starts, I'm not redesigning the relationship between truth and reads from scratch — I already know Django owns the record, FastAPI serves the projection, and the seam runs one way. That reuse is the quiet payoff of a shape you trust. The frameworks are interchangeable trivia; the seam is the idea, and the idea travels.