Add reflex-django to an existing Django project¶
This is the brownfield path. You already have a working Django project — models, migrations, admin, maybe a DRF API — and you just want to bolt on a reactive UI without rebuilding anything.
Good news: you don't have to touch your models or your existing views. You add reflex-django like any other Django app, list the URL prefixes you already own, and start dropping Reflex pages into your apps' views.py.
What you keep, what you add¶
| You keep | You add |
|---|---|
manage.py, models, migrations, admin |
reflex_django in INSTALLED_APPS |
Your existing /api/ and templates |
reflex_mount(...) in urls.py |
| Your custom middleware | AsyncStreamingMiddleware at the bottom of MIDDLEWARE |
| Your DRF views, webhooks, command-line scripts | @template pages inside any app's views.py |
Your settings.py (mostly) |
A few REFLEX_DJANGO_* keys (optional) |
You don't add an rxconfig.py. You don't add a {app}/{app}.py. You don't move things around. Promise.
1. Install¶
2. Register the app¶
In settings.py:
INSTALLED_APPS = [
# ... your existing apps ...
"reflex_django",
]
MIDDLEWARE = [
# ... your existing middleware ...
"reflex_django.streaming_middleware.AsyncStreamingMiddleware", # last
]
The streaming middleware needs to be last. It's there to keep Django's admin streaming responses ASGI-safe; it does nothing under WSGI. (Details.)
3. List your existing prefixes in reflex_mount()¶
This is the only step that needs care. reflex-django adds a catch-all URL pattern at the bottom of urls.py that serves the Reflex SPA for anything Django doesn't already own. So you need to tell it which prefixes Django does own.
# config/urls.py
from django.contrib import admin
from django.urls import include, path
from reflex_django.urls import reflex_mount
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("shop.api_urls")),
path("webhooks/", include("shop.webhooks_urls")),
]
urlpatterns += [
reflex_mount(
app_name="shop",
django_prefix=("/admin", "/api", "/webhooks"),
rx_config={"backend_port": 8000},
),
]
The rule is: every path() line above reflex_mount() should have a matching prefix in django_prefix. If you forget one, the SPA catch-all will try to serve the SPA shell when the user visits that URL.
If your URLs are organized into include()d files, just list the top-level prefix once (e.g. "/api" is enough to cover /api/products/, /api/orders/, etc.).
4. Point ASGI at reflex_django¶
If you have an existing config/asgi.py, replace it (or merge the import in):
# config/asgi.py
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
from reflex_django.asgi_entry import application # noqa: E402,F401
If you were previously deploying with gunicorn (WSGI), you'll want to switch to an ASGI server like uvicorn, granian, or hypercorn. (Deployment guide has examples.)
5. Add a Reflex page¶
Pick any Django app that's in INSTALLED_APPS (the same one you named in app_name, by default). Open its views.py and add a Reflex page:
# shop/views.py — your existing module
import reflex as rx
from reflex_django import template
from reflex_django.state import AppState
# ... your existing Django views can stay here, unchanged ...
class CatalogState(AppState):
products: list[dict] = []
@rx.event
async def load(self):
from shop.models import Product # import inside the handler is safer
self.products = [
{"id": p.id, "name": p.name, "price": str(p.price)}
async for p in Product.objects.filter(is_active=True)
]
@template(route="/catalog", title="Catalog", on_load=CatalogState.load)
def catalog() -> rx.Component:
return rx.vstack(
rx.heading("Catalog"),
rx.foreach(
CatalogState.products,
lambda p: rx.text(p["name"], " — $", p["price"]),
),
)
Notice: this file can hold both Django views (functions returning HttpResponse) and Reflex pages (functions returning rx.Component). They don't collide.
Why is the model import inside the handler? Because
views.pymay get imported before Django's app registry is ready. Putting model imports inside the handler dodgesAppRegistryNotReady. Once your project is fully booted, both patterns work, but the inside-handler version is the bulletproof one.
6. Run¶
http://localhost:8000/admin/— your admin, unchanged.http://localhost:8000/api/...— your existing API, unchanged.http://localhost:8000/catalog— your new Reflex page.
Your existing tests, management commands, and migrations all keep working.
How it sits beside your API¶
If you also have a DRF API at /api/, both surfaces coexist:
| Path | Who handles it |
|---|---|
/admin/... |
Your admin |
/api/... |
Your DRF / function views |
/webhooks/... |
Your webhook handlers |
/_event, /_upload, /_health |
Reflex internals (WebSocket, uploads, health) |
/, /catalog, /about, … |
Reflex SPA |
Same origin, same port, same cookies. Your mobile app can keep hitting /api/ while a browser sees the new SPA at /. No CORS to configure. (More on routing.)
Using your existing models¶
Inside any @rx.event handler on an AppState subclass, you can:
- Query the ORM with
async for,await Model.objects.aget(...),acreate,asave,adelete. - Read the logged-in user via
self.request.user— same user as/admin/. - Read or write the session via
self.request.session(orawait self.session.aset(...)for async). - Add flash messages via
messages.add_message(self.request, ...).
@rx.event
async def add_to_cart(self, product_id: int):
from shop.models import Cart, Product
product = await Product.objects.aget(pk=product_id)
cart, _ = await Cart.objects.aget_or_create(owner=self.request.user)
await cart.items.aadd(product)
Same model classes you've been using in your existing views and admin. No new layer.
What if my Reflex pages live in a different package?¶
Not all teams want UI code in the same app as models. That's fine. Either:
Option A — put pages in any other app's views.py. reflex-django auto-discovers views.py in every app listed in INSTALLED_APPS (skipping django.* and reflex_django itself):
INSTALLED_APPS = [
# ...
"shop", # models live here
"frontend", # frontend/views.py has the Reflex pages
]
Option B — point at an explicit list of page modules in settings.py:
That disables auto-discovery and uses only the modules you list. Good for monorepos with a clear separation between domain code and UI code.
Common bumps in a real project¶
AppRegistryNotReady
You imported a model at the top of views.py and that file got imported during early bootstrap. Move the import inside the handler:
404 on /api/orders/
You added a Django route but forgot to add the prefix to django_prefix. Add "/api" (top-level prefix is enough; you don't need to list every sub-path).
Port conflict with runserver
Only one process should bind the Reflex backend port. Don't run python manage.py runserver 8000 alongside python manage.py run_reflex.
Leftover rxconfig.py
If you previously experimented with plain Reflex, you might have a rxconfig.py at the project root pointing at {app}.{app}. Delete it. reflex_mount() is the only config you need.
Existing custom middleware not running on Reflex events
Good news — it does run, by default. The DjangoEventBridge walks your full settings.MIDDLEWARE chain on every Reflex event. If something specific isn't applying, see Custom middleware in events for the skip list and disable flags.
dispatch is not a function in the browser console
The compiled SPA's dispatcher map is out of sync with your live state classes. Stop the dev server, delete .web/, and restart python manage.py run_reflex. The next build re-generates a fresh dispatcher.
What's next¶
- Project structure — recommended file layout for a hybrid Django/Reflex project.
- Configuration — every
reflex_mount()argument and everyREFLEX_DJANGO_*setting. - CRUD with ModelState — declarative CRUD that pairs naturally with your existing Django models.
Next: Project structure →