Working with External Services
Calling REST APIs, Redis, and AWS from a LiveView without blocking the render.
On this page
How to integrate AWS, REST APIs, Redis, and other external services with djust LiveViews.
The Problem
djust LiveViews serialize their state to JSON between requests. This means everything stored as a public instance variable must be JSON-serializable. Service instances -- AWS clients, HTTP sessions, database connections, Redis clients -- are not serializable.
If you try to store a service instance in state:
# WRONG: boto3 client is not JSON-serializable
class S3BrowserView(LiveView):
template_name = "s3_browser.html"
def mount(self, request, **kwargs):
self.s3_client = boto3.client("s3") # Will fail on next render
self.buckets = []
By default djust logs LiveView state contains non-serializable value: … and stores a string in its place, so the next handler that calls a method on self.s3_client gets an AttributeError. With strict_serialization: True it raises TypeError instead (see Runtime Warnings and Errors).
The djust.V006 system check catches this pattern at startup.
Pattern 1: Helper Method (Recommended)
Create a private helper method that instantiates the service on demand. Since private methods (prefixed with _) are not serialized, this avoids the problem entirely.
import boto3
from djust import LiveView, state
from djust.decorators import event_handler
class S3BrowserView(LiveView):
template_name = "s3_browser.html"
buckets = state(default=[])
selected_bucket = state(default="")
objects = state(default=[])
def _get_s3_client(self):
"""Create a fresh S3 client for each request."""
return boto3.client("s3")
def mount(self, request, **kwargs):
client = self._get_s3_client()
response = client.list_buckets()
self.buckets = [b["Name"] for b in response["Buckets"]]
@event_handler()
def select_bucket(self, bucket_name: str = "", **kwargs):
self.selected_bucket = bucket_name
client = self._get_s3_client()
response = client.list_objects_v2(Bucket=bucket_name, MaxKeys=50)
self.objects = [
{"key": obj["Key"], "size": obj["Size"]}
for obj in response.get("Contents", [])
]
Why this works: The boto3.client() call happens inside the method, creates a fresh client, and the client reference is never stored on self as a public attribute. Only the serializable results (buckets, objects) are stored in state.
Pattern 2: Plain Dicts for External API Data
When you need to display data from an external API in a structured way, keep it as plain dicts. Don't wrap it in table-less (managed = False) model instances: djust stores model instances held in private state as a model reference and re-fetches them with objects.get(pk=...) when state is restored, so a model with no table comes back as None.
# views.py
import httpx
from djust import LiveView, state
from djust.decorators import event_handler
class ProductCatalogView(LiveView):
template_name = "catalog.html"
search_query = state(default="")
def _fetch_products(self, query=""):
"""Fetch products from external API and return them as plain dicts."""
response = httpx.get(
"https://api.example.com/products",
params={"q": query},
)
data = response.json()
return [
{
"id": item["id"],
"name": item["name"],
"price": item["price"],
"sku": item["sku"],
"in_stock": item["available"],
}
for item in data["results"]
]
def mount(self, request, **kwargs):
self._products = self._fetch_products()
@event_handler()
def search(self, value: str = "", **kwargs):
self.search_query = value
self._products = self._fetch_products(query=value)
def get_context_data(self, **kwargs):
self.products = self._products # JIT serialization
return super().get_context_data(**kwargs)
This pattern combines the JIT serialization pattern with plain-dict data for a clean separation.
Pattern 3: Dependency Injection
Pass services via mount() kwargs from your URL configuration. This is useful when you want to test views with mock services.
# views.py
from djust import LiveView, state
from djust.decorators import event_handler
from myapp.services import get_metrics_client # your factory
class DashboardView(LiveView):
template_name = "dashboard.html"
metrics = state(default=[])
def mount(self, request, **kwargs):
# Create the service ON-DEMAND and store it privately.
#
# URL kwargs DO reach `mount()` — `runtime.py:2117` does
# `mount_kwargs.update(self._resolve_url_kwargs(page_url))`, and
# `_resolve_url_kwargs` (`runtime.py:4096`) returns
# `dict(resolve(page_url).kwargs)`, which includes the extra-options
# dict of `path(route, view, {...})`. So URL-level injection is
# available. What is NOT available is passing a service through
# `live_session`: that is a URL-pattern grouping helper with the
# signature `live_session(prefix, patterns, session_name=None)`
# (`djust/routing.py:105`) and takes no view kwargs.
#
# A factory call here is still preferred for a live CLIENT: it keeps
# the object off `self` as public state, and a connection or client
# is not serializable.
self._metrics_client = get_metrics_client() # Private: not serialized
if self._metrics_client:
self.metrics = self._metrics_client.get_recent(limit=20)
@event_handler()
def refresh_metrics(self, **kwargs):
if self._metrics_client:
self.metrics = self._metrics_client.get_recent(limit=20)
Note that self._metrics_client uses the private prefix (_), so it is excluded from state serialization. For tests, monkeypatch get_metrics_client or inject a stub before calling mount().
What's Serializable
These types can be stored as public state variables:
| Type | Example |
|---|---|
| Primitives | str, int, float, bool, None |
| Collections | list, dict, tuple, set |
| Django models | Product.objects.get(pk=1) |
| QuerySets | Product.objects.filter(active=True) |
| Dates/times | datetime.date, datetime.datetime |
| UUIDs | uuid.UUID |
| Decimals | decimal.Decimal |
| Nested structures | {"items": [{"name": "Widget", "price": 9.99}]} |
What's Not Serializable
These types cannot be serialized as public state (by default djust warns and stores a string in their place; with strict_serialization: True it raises TypeError):
| Type | Example | Use Helper Method Instead |
|---|---|---|
| Service clients | boto3.client("s3") | self._get_s3_client() |
| HTTP sessions | httpx.Client(), requests.Session() | self._get_http_client() |
| Database connections | psycopg2.connect(...) | Use Django ORM |
| Redis clients | redis.Redis() | self._get_redis() |
| Open file handles | open("data.csv") | Read and close in method |
| Thread/process objects | threading.Thread(...) | Use Celery tasks |
| WebSocket connections | websockets.connect(...) | Use push_event |
| Generators | (x for x in range(10)) | Convert to list() |
| Lambda functions | lambda x: x + 1 | Use regular methods |
Detection and Debugging
System Checks: V006 and V008
djust provides two static checks to catch non-serializable state at development time:
python manage.py check --tag djust
V006 (warning) detects service-like instances: a self.x = Call(...) in mount() whose call name contains "Service", "Client", "Session", "API", or "Connection" (case-insensitive):
(djust.V006) myapp/views.py:15 -- Service instance 'api_client' assigned in mount(). Service instances cannot be serialized.
HINT: Use a helper method pattern instead. See: docs/guides/services.md
V008 (info, broader check, issue #292) detects any non-primitive type assignment in mount():
(djust.V008) myapp/views.py:15 -- Non-primitive type 'MyServiceClass' assigned to self.service in mount().
Ensure this type is JSON-serializable.
HINT: If 'MyServiceClass' is not serializable, use self._service instead or re-initialize in event handlers.
V008 catches a broader set of potential serialization issues beyond just service instances.
Runtime Warnings and Errors
Default behavior (strict_serialization=False):
When a non-serializable object is encountered at runtime, djust logs a warning and converts it to a string as a fallback:
WARNING [djust.serialization] LiveView state contains non-serializable value: S3Client (from botocore.client).
This will be converted to a string, which may cause AttributeError on deserialization.
Consider using self._<attr> for private state, or re-initialize in mount()/event handlers.
On the next request, deserialized state will contain a string like "<botocore.client.S3 object at 0x...>" instead of the original service instance, causing AttributeError when you try to call methods on it.
Strict mode (strict_serialization=True):
Enable strict mode to catch these issues early during development:
# settings.py
LIVEVIEW_CONFIG = {
'strict_serialization': True, # Raise TypeError instead of str() fallback
}
With strict mode enabled, non-serializable values raise an immediate TypeError with an actionable error message:
TypeError: LiveView state contains non-serializable value: S3Client (from botocore.client).
This will be converted to a string, which may cause AttributeError on deserialization.
Consider using self._<attr> for private state, or re-initialize in mount()/event handlers.
The stack trace will point to the serialization step. The fix is always the same: move the service to a private variable or a helper method.
Debugging Steps
- Find the public attribute causing the error (look at the
TypeErrormessage) - Rename it with a
_prefix to make it private:self.client->self._client - Create a helper method if you need to re-create the service:
self._get_client() - Store only the serializable results in public state
Common Patterns
AWS / Boto3
class S3View(LiveView):
template_name = "s3.html"
files = state(default=[])
def _s3(self):
return boto3.client("s3")
def mount(self, request, **kwargs):
resp = self._s3().list_objects_v2(Bucket="my-bucket")
self.files = [obj["Key"] for obj in resp.get("Contents", [])]
@event_handler()
def delete_file(self, key: str = "", **kwargs):
self._s3().delete_object(Bucket="my-bucket", Key=key)
self.files = [f for f in self.files if f != key]
External REST APIs
import httpx
class WeatherView(LiveView):
template_name = "weather.html"
city = state(default="London")
forecast = state(default={})
def _fetch_weather(self, city):
resp = httpx.get(
"https://api.weather.example.com/forecast",
params={"city": city},
)
return resp.json()
def mount(self, request, **kwargs):
self.forecast = self._fetch_weather(self.city)
@event_handler()
def change_city(self, value: str = "", **kwargs):
self.city = value
self.forecast = self._fetch_weather(value)
Redis Cache
import redis
class LeaderboardView(LiveView):
template_name = "leaderboard.html"
scores = state(default=[])
def _redis(self):
return redis.Redis(host="localhost", port=6379, db=0)
def mount(self, request, **kwargs):
self._load_scores()
def _load_scores(self):
r = self._redis()
raw = r.zrevrange("leaderboard", 0, 9, withscores=True)
self.scores = [
{"name": name.decode(), "score": int(score)}
for name, score in raw
]
@event_handler()
def refresh(self, **kwargs):
self._load_scores()
See Also
- Best Practices -- State management conventions
- JIT Serialization Pattern -- Private/public variable pattern
- Error Codes -- V006 and other check details