Auto-generated HTTP API
djust already has everything an HTTP API needs: routing by handler name, parameter coercion from JSON, permission checks, rate limiting, and typed validation. The expose_api flag flips the transport — same handler, same security model, same validation, but callable over plain HTTP/JSON without a WebSocket.
What You Get
POST /djust/api/<view_slug>/<handler_name>/— one endpoint per marked handler- OpenAPI 3.1 schema at
/djust/api/openapi.json— auto-generated from handler type hints and@event_handlermetadata - Shared auth: Django session cookies + CSRF by default; pluggable token auth via
api_auth_classes - Shared rate-limiting bucket with the WebSocket transport
api_response()helper for transport-conditional returns (HTTP vs WS)
Quick Start
1. Mark handlers to expose
from djust import LiveView
from djust.decorators import event_handler, permission_required
class OrderView(LiveView):
template_name = 'order.html'
@event_handler(expose_api=True)
def update_status(self, order_id: int, status: str, **kwargs):
order = Order.objects.get(pk=order_id)
order.status = status
order.save(update_fields=['status'])
return {'order_id': order_id, 'status': status}
@event_handler(expose_api=True)
@permission_required('orders.change_order')
def refund(self, order_id: int, reason: str = '', **kwargs):
order = Order.objects.get(pk=order_id)
order.status = 'refunded'
order.save(update_fields=['status'])
return {'order_id': order_id, 'refunded': True}
2. Point your client at the schema
GET /djust/api/openapi.json
Returns a valid OpenAPI 3.1 document. Every expose_api=True handler appears as a POST path under /djust/api/<view_slug>/<handler_name>/. The schema includes:
requestBodyderived from the handler's type hintsparametersfor path/query bindingresponseswith error codes (400 validation, 401 unauthenticated, 403 forbidden, 404 not found, 429 rate-limited, 500 handler exception)securityfromapi_auth_classes
3. Call it
curl -X POST https://yourapp.com/djust/api/order/update_status/ \
-H "Content-Type: application/json" \
-H "X-CSRFToken: $(python -c "from django.templatetags.csrf import get_token; print(get_token())")" \
-d '{"order_id": 42, "status": "shipped"}'
Response (success):
{
"result": {"order_id": 42, "status": "shipped"},
"assigns": {}
}
Response (validation error):
{
"error": "validation_error",
"details": {
"order_id": ["This field is required."]
}
}
URL shape
POST /djust/api/<view_slug>/<handler_name>/
<view_slug>is derived from the view's URL name (the name Django's URL resolver assigns). Override with a class-levelapi_nameattribute on the view.- Named parameters come from the request body JSON.
- Positional parameters are not supported over HTTP — handlers that rely on positional binding from
dj-click="handler('v')"must still declare named parameters to be callable over HTTP.
Response envelope
{
"result": <handler return value or null>,
"assigns": { <changed public assigns only> }
}
assigns reflects what changed during the handler run. djust uses the same diff machinery it uses for WebSocket responses — only the assigns that actually changed are included.
Transport-conditional returns
Handlers that need to behave differently over HTTP vs WebSocket can use api_response():
from djust.api import api_response
@event_handler(expose_api=True)
def get_chart_data(self, metric: str, **kwargs):
data = compute_metric(metric)
if self._http_request:
# Plain HTTP caller wants the raw data
return api_response(result=data)
# WebSocket caller — let djust patch assigns normally
self.chart_data = data
api_response(result=, serialize=, status=) wraps the return:
| kwarg | effect |
|---|---|
result=... | Puts the value in the result field; assigns are not diffed |
serialize=... | Callable to run on the result before serialization (e.g., a Pydantic model) |
status=... | HTTP status code override (e.g., status=201) |
Without api_response, the return value is treated as a view assign (the normal WS behavior) and djust computes the diff.
Auth
By default the HTTP transport uses Django's session auth plus CSRF — the same as a browser form submit. Pluggable via api_auth_classes:
from djust.auth import SessionAuth
class OrderView(LiveView):
api_auth_classes = [SessionAuth] # default; can be omitted
For token-based clients (mobile, CLI, server-to-server), implement the auth class contract:
class TokenAuth:
def authenticate(self, request):
# Return the user object, or None to fall through to the next class
token = request.headers.get('Authorization', '').replace('Bearer ', '')
try:
return User.objects.get(api_token=token)
except User.DoesNotExist:
return None
If no auth class authenticates, djust returns 401 Unauthorized.
Set csrf_exempt = True on a class to disable CSRF for that view (header/token auth doesn't need it):
class OrderView(LiveView):
api_auth_classes = [TokenAuth]
csrf_exempt = True
Permission flow
Handler-level @permission_required(...) and view-level login_required / permission_required run identically over HTTP and WebSocket. The same 401 / 403 response codes apply.
@event_handler(expose_api=True)
@permission_required('orders.change_order') # 403 if user lacks the permission
def refund(self, order_id: int, **kwargs):
...
OpenAPI schema
The schema is live — it updates whenever handler signatures change. Regenerate it on demand:
GET /djust/api/openapi.json
The schema includes:
paths— onePOSTperexpose_api=Truehandler- Request body — JSON object with the handler's named parameters; type hints map to JSON Schema types (
int→integer,bool→boolean,str→string,UUID→stringwithformat: uuid) - Response
200—{result, assigns}envelope - Response
400/401/403/404/429/500—{error, details}envelope security— reflectsapi_auth_classes(e.g.,CookieforSessionAuth)
Customizing the schema
Add a description or deprecate a handler via the decorator:
@event_handler(
expose_api=True,
description="Mark an order as shipped. Idempotent.",
deprecated=False,
)
def update_status(self, order_id: int, status: str, **kwargs):
...
Rate limiting
The HTTP and WebSocket transports share the same per-handler token bucket. A handler limited to 60 calls/minute via rate_limit(...) will count HTTP calls and WS calls in the same bucket.
Error codes
| Status | error field | Meaning |
|---|---|---|
400 | validation_error | Handler params failed type coercion or @field_validator |
401 | unauthenticated | No valid session / token |
403 | permission_denied | Authenticated but lacks required permission |
404 | not_found | View or handler not found, or expose_api=True not set |
429 | rate_limited | Per-handler rate limit exceeded |
500 | handler_error | Handler raised an exception; details in details.traceback (DEBUG only) |
Limitations
- No file upload over HTTP. The chunked WebSocket upload path is separate. Use the WebSocket transport for uploads.
- Stateless only. Each HTTP request is an independent call — there is no session binding. If your handler needs session state, populate it from the database on each call.
- No streaming responses. Handlers that return
StreamingHttpResponsecannot be exposed viaexpose_api. - Handlers must accept named kwargs. The HTTP transport passes all params as keyword arguments. Positional-only handlers cannot be called over HTTP.
Exposing a handler: checklist
@event_handler(expose_api=True) # 1. Opt in
def my_handler(self, param: int, **kwargs): # 2. Typed params → schema
...
return api_response(result=...) # 3. (optional) transport-conditional
That's it. djust wires the URL, generates the schema entry, and dispatches the call — reusing every security and validation check you already have on the WebSocket path.