Build interactive Django apps without splitting your product into "backend API + SPA frontend".
HyperDjango keeps rendering and business logic on the server, then layers in partial swaps, signals, and transitions for SPA-like UX.
- Problem: teams duplicate feature logic across Django views, API serializers, and frontend state stores.
- Approach: treat HTML as the interface, organize features in
hyper/, and use actions for incremental updates. - Outcome: one feature implementation path with fast interactions and fewer moving parts.
- Keep business logic in Django, not duplicated across REST + frontend app layers.
- Get SPA-like interactions (partial swaps, toasts, transitions) with HTML as the transport.
- Organize by feature using file-based routes and co-located templates/assets.
HyperDjango is centered around a single hyper/ directory where routes, layouts, and frontend entry files live together.
hyper/
layouts/
base/
__init__.py
index.html
entry.ts
routes/
index/
page.py
index.html
partials/
flash.html
todos/
page.py
index.html
partials/
item.html
shared/
__init__.py
Each route folder owns its behavior:
page.py: server logic (get/post/@action)index.html: page templatepartials/*: fragments used by action swaps- nearby
entry.tsfiles: route/layout-specific client assets
This keeps the code you change for a feature close together.
- file-based routing from
hyper/routes/**/+page.py - nested
layout.pycomposition - class-based pages with native
get/post/...handlers @actionmethods for hypermedia interactions- Alpine-friendly
$action(name, data, options)client helper - progressive enhancement for links/forms (
hyper-nav) - native Vite integration (dev server + build manifest)
Package: https://pypi.org/project/hyperdjango/
From PyPI (recommended):
uv add hyperdjangoor:
pip install hyperdjango- Add app + settings:
INSTALLED_APPS = [
# ...
"hyperdjango",
]
HYPER_FRONTEND_DIR = BASE_DIR / "hyper"
HYPER_VITE_OUTPUT_DIR = BASE_DIR / "dist"
HYPER_VITE_DEV_SERVER_URL = "http://localhost:5173/"
HYPER_DEV = DEBUG
TEMPLATES[0]["DIRS"].append(HYPER_FRONTEND_DIR)
STATICFILES_DIRS = [HYPER_VITE_OUTPUT_DIR]- Scaffold HyperDjango into your existing project:
python manage.py hyper_scaffoldThis generates hyper/ directories (including routes/, templates/, layouts/), starter files, vite.config.js, and package.json dependencies/scripts. By default it also wires your Django settings + urls file. Use --no-wire to skip patching, or --force to overwrite scaffolded files.
- If you used
--no-wire, mount routes manually inurls.py:
from django.contrib import admin
from django.urls import path
from hyperdjango.urls import include_routes
urlpatterns = [
path("admin/", admin.site.urls),
*include_routes(),
]- Run route introspection:
python manage.py hyper_routesfrom hyperdjango.page import HyperView
class PageView(HyperView):
def get(self, request):
return {"title": "About"}Place this in hyper/routes/about/+page.py and create hyper/routes/about/index.html.
Note: each hyper/routes/**/+page.py must define a class named PageView (typically inheriting HyperView).
Use @action for partial updates, signal patches, toasts, and swap control.
from hyperdjango.actions import action
from hyperdjango.page import HyperView
class PageView(HyperView):
@action
def add(self, request, title=""):
html = self.render_block(
request=request,
block_name="todo_list",
context_updates={"items": [title]},
)
return self.action_response(
html=html,
target="#todo-list",
swap="inner",
toast={"type": "success", "message": "Added"},
)If you do not want file-based URL routing for a feature, create a template package in hyper/templates/** and render it from your own Django view.
from hyperdjango.page import HyperPageTemplate
class ProfileCardTemplate(HyperPageTemplate):
passfrom hyperdjango.shortcuts import render_template_page
def profile_card(request):
return render_template_page(request, ProfileCardTemplate, context={"title": "Account"})HyperDjango exposes helpers globally and as Alpine magics:
window.action(...)- Alpine:
$action(...)
<div x-data="{ q: '' }">
<input x-model="q" />
<button x-on:click="$action('search', { q }, { target: '#results', key: 'search' })">
Search
</button>
</div>routes/index/+page.py->/routes/about/+page.py->/aboutroutes/blog/[slug]/+page.py->/blog/<slug>routes/blog/[str__slug]/+page.py->/blog/<str:slug>routes/docs/[...path]/+page.py->/docs/<path:path>routes/accounts/reset/[uidb36]-[key]/+page.py->/accounts/reset/<uidb36>-<key>routes/account/reset/[uid__[0-9A-Za-z]+]-[key__.+]/+page.py-> inline regex segmentroutes/(marketing)/pricing/+page.py->/pricing
Reverse URL names are generated automatically (for example routes/blog/[slug]/+page.py -> hyper_blog_slug) and can be overridden per page with route_name on PageView.
Load tags:
{% load hyper_tags %}Available tags:
hyper_preloadshyper_stylesheetshyper_head_scriptshyper_body_scriptshyper_custom_entry "name"
-
ReadTheDocs structure is configured via
.readthedocs.yamlanddocs/conf.py. -
Docs sections are split into Guides, Reference, and Examples/Cookbook.
A full runnable demo lives in example/.
- setup and run instructions: example/README.md
- includes routes for static, dynamic, catch-all, grouped, nested layouts
- includes action-heavy examples:
/todos,/search,/profile