Skip to content
djust/docs
Appearance
Mode
djust.org →
Browse documentation

4 min read

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_handler metadata
  • 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:

  • requestBody derived from the handler's type hints
  • parameters for path/query binding
  • responses with error codes (400 validation, 401 unauthenticated, 403 forbidden, 404 not found, 429 rate-limited, 500 handler exception)
  • security from api_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-level api_name attribute 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:

kwargeffect
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 — one POST per expose_api=True handler
  • Request body — JSON object with the handler's named parameters; type hints map to JSON Schema types (intinteger, boolboolean, strstring, UUIDstring with format: uuid)
  • Response 200{result, assigns} envelope
  • Response 400/401/403/404/429/500{error, details} envelope
  • security — reflects api_auth_classes (e.g., Cookie for SessionAuth)

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

Statuserror fieldMeaning
400validation_errorHandler params failed type coercion or @field_validator
401unauthenticatedNo valid session / token
403permission_deniedAuthenticated but lacks required permission
404not_foundView or handler not found, or expose_api=True not set
429rate_limitedPer-handler rate limit exceeded
500handler_errorHandler 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 StreamingHttpResponse cannot be exposed via expose_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.

Spotted a typo or want to improve this page? Edit on GitHub →