nanodjango integration¶
nanodjango is a single-file Django framework for quick demos, prototypes, and small apps. Django-Bolt ships a nanodjango plugin that lets you run Bolt's Rust/Actix server inside a nanodjango app without manually wiring INSTALLED_APPS, BOLT_API, or import order -- just import BoltAPI from django_bolt.nanodjango and start defining routes.
Install¶
Or with uv:
This pulls in nanodjango as an extra and registers Django-Bolt as a nanodjango plugin automatically.
Quickstart¶
A complete single-file app, helloworld.py. With uv's inline script metadata, you can share it as a self-installing example:
# /// script
# dependencies = [
# "django-bolt[nanodjango]",
# ]
# ///
from nanodjango import Django
from django_bolt.nanodjango import BoltAPI
app = Django()
bolt = BoltAPI()
# Django route (WSGI)
@app.route("/")
def home(request):
return r"""<h1>Hello, django-bolt + nanodjango!</h1>
Check out the autogenerated <a href="/docs/">API docs</a>.
<br>Invoke the API directly:
<p>Try <a href="/api/hello">/api/hello</a> or
<a href="/api/greet?name=World">/api/greet?name=World</a></p>"""
# Bolt API routes
@bolt.get("/api/hello")
async def hello(request):
return {"message": "hello from bolt"}
@bolt.get("/api/greet")
async def greet(name: str):
return {"message": f"hello, {name}!"}
@bolt.post("/api/echo")
async def echo(msg: str):
return {"you_sent": msg}
# Register the Django catch-all AFTER all bolt routes so they take priority.
# Unmatched requests (like /) are forwarded to Django's ASGI app.
bolt.mount_django(r"/")
if __name__ == "__main__":
import sys
# Imported after Django settings are configured (single-file style)
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
Run it with uv -- it creates a temporary environment, downloads dependencies, and executes the script:
Using the nanodjango CLI¶
Alternatively, use nanodjango's management CLI. In this case you don't need the if __name__ == "__main__" block:
What it does¶
Without this plugin, using Django-Bolt in a single-file nanodjango app requires manually configuring INSTALLED_APPS, BOLT_API, and getting the import order right (django-bolt must be imported after Django settings are configured).
The django_bolt.nanodjango plugin handles all of that:
- Adds
django_bolttoINSTALLED_APPSautomatically - Configures
BOLT_API(the setting that tellsrunboltwhere to find your API instance) by detecting the variable name at decoration time - Subclasses
django_bolt.BoltAPIsorunbolt's autodiscovery accepts it - Registers as a nanodjango plugin via setuptools entry points (no config needed)
- Supports
nanodjango convertby detectingBoltAPIinstances and route-decorated functions in the AST and moving them toapi.pyin the generated project
Running the server¶
Django-Bolt runs a Rust/Actix server, not Django's dev server. You have a few deployment patterns to choose from.
Single port (Django views + Bolt API together)¶
Use mount_django to serve everything on one port. Bolt routes are handled natively by Actix; all other requests are forwarded to Django's ASGI application through an in-process bridge.
from nanodjango import Django
from django_bolt.nanodjango import BoltAPI
app = Django()
api = BoltAPI()
@app.route("/")
def home(request):
return "<h1>Hello from Django</h1>"
@api.get("/api/hello")
async def hello(request):
return {"message": "hello from bolt"}
# Mount Django as the fallback. Must be called AFTER all BoltAPI routes
# are defined, since bolt routes take priority.
api.mount_django(r"/")
Because the file must be importable, avoid non-importable characters in the filename -- use myapp.py, not my-app.py.
Bolt only (no Django view fallback)¶
If you don't need Django views served over HTTP, skip mount_django:
Note that if you have called api.mount_django("/path"), Django will still be served over HTTP under that prefix.
Separate ports (Django dev + Bolt on different ports)¶
Useful during development if you want separate logs / restarts for each server:
# Terminal 1 -- Django views
nanodjango run myapp.py --host localhost:8080
# or: nanodjango manage myapp.py runserver --host localhost:8080
# Terminal 2 -- Bolt API
nanodjango manage myapp.py runbolt --port 8001
Production mode¶
Run Django under uvicorn via nanodjango serve and Bolt via runbolt, then put a reverse proxy (nginx, Caddy, etc.) in front of both:
# Terminal 1 -- Django (uvicorn)
nanodjango serve myapp.py --host localhost:8080
# Terminal 2 -- Bolt API
nanodjango manage myapp.py runbolt --port 8001
# Then configure your reverse proxy to route public traffic to both.
runbolt options¶
| Flag | Default | Description |
|---|---|---|
--host |
0.0.0.0 |
Host to bind to |
--port |
8000 |
Port to bind to |
--processes |
1 |
Number of worker processes |
--dev |
off | Auto-reload on file changes |
--no-admin |
off | Disable Django admin integration |
--backlog |
1024 |
Socket listen backlog |
--keep-alive |
OS default | HTTP keep-alive timeout (seconds) |
Streaming responses (SSE)¶
For Server-Sent Events, use EventSourceResponse. It handles JSON encoding, SSE wire framing (data: ...\n\n), keep-alive pings, and skipping compression -- compression buffers stream chunks and breaks real-time delivery.
import asyncio
from collections.abc import AsyncIterable
from nanodjango import Django
from django_bolt.nanodjango import BoltAPI
from django_bolt.responses import EventSourceResponse
app = Django()
bolt = BoltAPI()
@bolt.get("/stream", response_class=EventSourceResponse)
async def stream() -> AsyncIterable[dict]:
for i in range(5):
yield {"message": f"message {i}"}
await asyncio.sleep(1)
Yielded objects are JSON-serialized and wrapped in SSE framing automatically. See the Server-Sent Events guide for the explicit form, custom event types, IDs, retry intervals, and ping configuration.
If you need raw bytes / manual framing, the legacy StreamingResponse + @no_compress decorator combination still works -- but EventSourceResponse is the recommended path.
WebSockets¶
WebSocket endpoints work the same way as in a non-nanodjango Bolt app -- the plugin doesn't add anything special. Decorate with @bolt.websocket(path) and accept a WebSocket parameter:
from nanodjango import Django
from django_bolt import WebSocket, WebSocketDisconnect
from django_bolt.nanodjango import BoltAPI
app = Django()
bolt = BoltAPI()
@bolt.websocket("/ws/echo")
async def echo(websocket: WebSocket):
await websocket.accept()
try:
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"echo: {msg}")
except WebSocketDisconnect:
pass
A connecting client (browser):
const ws = new WebSocket(`ws://${location.host}/ws/echo`);
ws.onmessage = (e) => console.log(e.data);
ws.onopen = () => ws.send("hello");
Path parameters, JSON helpers, guards, and auth all work the same as documented in the WebSocket guide.
Constructing inside a factory¶
The plugin uses frame inspection at BoltAPI() construction time to figure out which module owns the instance, so it can set BOLT_API correctly. This works as long as you call BoltAPI() at module top level (the typical single-file pattern).
If you wrap construction in a factory or helper, frame inspection will pick up the wrong module and runbolt won't find your routes. Pass module=__name__ explicitly to bypass it:
def make_bolt():
bolt = BoltAPI(module=__name__) # or "myapp" / wherever the binding lives
...
return bolt
bolt = make_bolt()
The plugin emits a warning to the django_bolt.nanodjango logger when auto-detection fails, so you'll see the issue at startup rather than getting a silent empty router.
Converting to a full project¶
When you outgrow the single-file format, nanodjango can scaffold a full Django project:
The plugin's convert_build_app_api hook detects BoltAPI() assignments and @bolt.get/post/... decorated functions and moves them into api.py in the generated project.
Note
Due to how nanodjango is written, you will get some superfluous code referring to django-ninja calls in the generated project. Strip or adapt those references after the conversion.
Comparison: with and without the plugin¶
Without the plugin -- manual wiring:
import sys
from django.conf import settings
settings.configure(
DEBUG=True,
SECRET_KEY="change-me",
ROOT_URLCONF=__name__,
INSTALLED_APPS=[
"django.contrib.contenttypes",
"django.contrib.auth",
"django_bolt",
],
BOLT_API=["__main__:api"],
)
from django_bolt import BoltAPI # must be after settings.configure()
api = BoltAPI()
@api.get("/hello")
async def hello(request):
return {"message": "hello"}
if __name__ == "__main__":
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
With the plugin: