Django-Bolt

repository·master·Indexed 23 days ago

https://github.com/dj-bolt/django-bolt

A high-performance, fully typed API framework for Django featuring a Rust-powered backend (Actix Web, Tokio, PyO3) and msgspec serialization. It maintains full compatibility with the Django ORM and ecosystem while providing speeds comparable to FastAPI. Includes bolt-mcp for building Model Context Protocol (MCP) servers with support for streaming tools, bidirectional communication (sampling and elicitation), and OAuth 2.1 resource server implementation.

Tokens
111.9K
Snippets
308
Records
442
Agent score
81%

What's inside django-bolt

  1. Overview of Django-Bolt features

    master

    Django-Bolt is a high-performance API framework for Django that uses a Rust-powered HTTP server (Actix Web + Tokio + PyO3) and msgspec for fast serialization.

    Key capabilities include:

    • High Performance: Rust-powered routing and HTTP handling.
    • Authentication & Permissions: JWT/API Key validation in Rust and route protection via IsAuthenticated and Requires claim checks.
    • Middleware: Support for CORS, rate limiting, compression, and Django middleware integration.
    • Serializers: msgspec-based validation.
    • Django ORM: Full async ORM support.
    • Responses: Support for JSON, HTML, streaming, SSE, and file downloads.
    • OpenAPI: Auto-generated documentation (Swagger, ReDoc, Scalar, RapidDoc).
    • Class-Based Views: ViewSet and ModelViewSet patterns.
    • Testing: Built-in test client.
  2. Understand the validation layers and error collection

    master

    Django-Bolt uses two validation layers and collects all errors into a single RequestValidationError (similar to Pydantic) when using model_validate() or model_validate_json():

    1. Meta constraints: Declarative constraints defined using Annotated and msgspec.Meta (e.g., min_length, pattern, ge). These are high-performance and handled by msgspec.
    2. Custom validators: Logic defined via @field_validator and @model_validator.

    Validation Order:

    1. msgspec parses and validates (Type checking + Meta constraints).
    2. @field_validator runs.
    3. @model_validator runs.
    4. All errors from all layers are raised together.
    from typing import Annotated
    from msgspec import Meta
    from django_bolt.serializers import Serializer, field_validator
    
    class UserSerializer(Serializer):
        # Layer 1: Meta constraints
        name: Annotated[str, Meta(min_length=2)]
        email: str
    
        # Layer 2: Custom field validator
        @field_validator("email")
        def validate_email(cls, value):
            if "@" not in value:
                raise ValueError("Invalid email")
            return value
  3. Implement Broadcast and Room-based patterns

    master

    Real-time communication often requires sending messages to multiple clients.

    Broadcast to all clients

    Maintain a global set of connected WebSocket objects. When a message is received, iterate through the set and call send_text on each client. Use a finally block to ensure clients are removed from the set when they disconnect.

    Room-based chat

    Maintain a mapping of room_id to a set of WebSocket objects. When a client connects to a specific room path, add them to that room's set. Broadcast messages only to the clients within that specific room.

    # Broadcast pattern
    connected_clients = set()
    
    @api.websocket("/ws/broadcast")
    async def broadcast(websocket: WebSocket):
        await websocket.accept()
        connected_clients.add(websocket)
        try:
            async for message in websocket.iter_text():
                for client in connected_clients:
                    await client.send_text(message)
        finally:
            connected_clients.discard(websocket)
    
    # Room-based pattern
    rooms = {}  # room_id -> set of websockets
    
    @api.websocket("/ws/room/{room_id}")
    async def room(websocket: WebSocket, room_id: str):
        await websocket.accept()
        if room_id not in rooms:
            rooms[room_id] = set()
        rooms[room_id].add(websocket)
        try:
            async for message in websocket.iter_text():
                for client in rooms[room_id]:
                    await client.send_text(f"[{room_id}] {message}")
        finally:
            rooms[room_id].discard(websocket)
  4. Understand Benchmark Metrics

    master

    When reviewing benchmark results, focus on these key performance indicators:

    • Reqs/sec: Requests per second. A higher value indicates better performance.
    • Latency: The time taken to process a request. Lower values are better.
    • Latency Distribution: Provides percentile-based latency measurements:
      • 50% (p50): Median latency.
      • 75% (p75): Latency below which 75% of requests fall.
      • 90% (p90): Latency below which 90% of requests fall.
      • 99% (p99): Latency below which 99% of requests fall (tail latency).
  5. Understand MCP Transport and Session Modes

    master

    The mount_mcp method registers POST, GET, and DELETE endpoints on /mcp:

    • POST: Handles JSON-RPC requests. By default, responses are streamed as text/event-stream (MCP-SDK-faithful). Use MCP(json_response=True) for a single application/json object (multi-process friendly).
    • GET: Opens the long-lived SSE (Server-Sent Events) listen channel for server-to-client messages.
    • DELETE: Terminates the session.

    Session Management

    Sessions are tracked via the Mcp-Session-Id header.

    • Stateful Mode (Default): Required for bidirectional features like sampling and elicitation. Requires a single worker (runbolt --processes 1) or sticky sessions.
    • Stateless Mode: Use MCP(stateless=True). This disables the GET channel; every POST is self-contained. report_progress and logging work in this mode, but bidirectional communication does not.
  6. Understand routing precedence and middleware boundaries for ASGI mounts

    master

    When using ASGI mounts in Django-Bolt, be aware of how they interact with the rest of the system:

    Routing Precedence:

    1. Bolt routes are matched first.
    2. ASGI mounts act as a fallback only if no Bolt route matches.

    Because Bolt routes take precedence, a mounted app cannot override a matching Bolt route. Additionally, Bolt's API semantics (like method mismatch or trailing-slash redirects) are resolved before the mount fallback occurs.

    Middleware Boundary: ASGI mounts run outside the Bolt route middleware pipeline. Bolt-level features such as rate limits, Bolt auth guards, and Bolt CORS behavior do not apply to mounted apps. You must configure these concerns using standard Django middleware or settings within the mounted application.

  7. How dependencies work in Django-Bolt

    master

    Django-Bolt uses a dependency injection system via the Depends marker. A dependency is any callable (a function or a class) that returns a value. When a handler parameter is annotated with Depends(), Django-Bolt performs the following lifecycle:

    1. Calls the dependency function: Executes the callable.
    2. Passes the result to the handler: Injects the return value into the endpoint function.
    3. Caches the result: By default, the result is cached for the duration of the same request. If the same dependency is requested multiple times within one request, the cached result is reused instead of re-executing the logic.

    Dependencies can be synchronous or asynchronous, and they can be nested (one dependency depending on another).

    from django_bolt import BoltAPI, Depends
    
    api = BoltAPI()
    
    async def get_current_user(request):
        # ... logic ...
        return user
    
    @api.get("/profile")
    async def get_profile(user=Depends(get_current_user)):
        return {"id": user.id, "username": user.username}
  8. Handle RequestValidationError for input validation

    master

    When request data fails validation, Django-Bolt raises a RequestValidationError which returns a 422 Unprocessable Entity status.

    Django-Bolt collects all errors from both Meta constraints and custom @field_validator/@model_validator functions before raising the exception, allowing for multi-error reporting.

    Accessing errors programmatically:

    try:
        serializer = MySerializer(email="bad", password="short")
    except RequestValidationError as e:
        all_errors = e.errors()  # Returns list of error dicts
        print(str(e))  # "body.email: Invalid email; body.password: Too short"

    Response Format:

    {
        "detail": [
            {
                "loc": ["body", "email"],
                "msg": "Invalid email",
                "type": "value_error"
            },
            {
                "loc": ["body", "password"],
                "msg": "Password too short",
                "type": "value_error"
            }
        ]
    }
  9. How graceful worker recycling works

    master

    Recycling is spawn-first and graceful, making it safe for long-lived connections like WebSockets:

    1. Replacement First: The supervisor forks a replacement worker before stopping the old one. SO_REUSEPORT allows both to share the port.
    2. Draining: The old worker receives SIGTERM and stops accepting new connections. The kernel routes new traffic to healthy workers.
    3. WebSocket Handling: Active WebSocket connections receive a proper close frame with code 1012 (Service Restart). Clients must be built with reconnection logic to handle this.
    4. HTTP Completion: In-flight HTTP requests are allowed to finish up to the --workers-kill-timeout (or the DJANGO_BOLT_SHUTDOWN_TIMEOUT env var, default 30s).
    5. Force Kill: If the worker hasn't exited after the timeout, it is SIGKILLed.

    Note on WebSockets: No server can migrate a live TCP connection. Use Redis or a database to persist per-connection session state so reconnections are transparent to users.

  10. Use nested serializers for complex data structures

    master

    Serializers can nest other serializers to represent complex, hierarchical data.

    • Basic Nesting: Simply use the nested serializer class as a type annotation for a field.
    • Lists of Nested Serializers: Use list[NestedSerializer] for collections.
    • Custom Metadata: Use Annotated[list[NestedSerializer], Nested(max_items=N)] to provide extra metadata like list limits via the Nested helper.
    from typing import Annotated
    from django_bolt.serializers import Serializer, Nested
    
    class AddressSerializer(Serializer):
        street: str
        city: str
        zip_code: str
    
    class UserSerializer(Serializer):
        id: int
        name: str
        address: AddressSerializer
    
    class PostSerializer(Serializer):
        id: int
        title: str
        tags: Annotated[list[TagSerializer], Nested(max_items=200)]
  11. Understand Django-Bolt validation layers and error collection

    master

    Django-Bolt's Serializer uses two layers of validation. The behavior regarding error collection depends on how you trigger validation:

    1. Meta constraints (e.g., min_length, pattern, ge, le in Meta class).
    2. Custom validators (@field_validator, @model_validator).

    Error Collection Behavior

    MethodExceptionMulti-error?
    Serializer.model_validate(dict)RequestValidationErrorYes
    Serializer.model_validate_json(json)RequestValidationErrorYes
    Serializer(field=value) (direct)RequestValidationErrorYes (custom only)
    msgspec.convert(data, Serializer)msgspec.ValidationErrorNo (fail-fast)
    msgspec.json.decode(json, type=Serializer)msgspec.ValidationErrorNo (fail-fast)

    Note: Direct instantiation (Serializer(field=value)) runs custom validators but bypasses Meta constraints for performance reasons.

    Example of multi-error collection:

    try:
        UserSerializer.model_validate({
            "name": "X",        # Meta: min_length=2 violated
            "email": "bad",     # Meta: pattern violated
            "age": 200          # Meta: le=150 violated
        })
    except RequestValidationError as e:
        print(len(e.errors()))  # 3 errors, not just 1
  12. Manage memory usage for high-fanout SSE with brotli_lgwin

    master

    When using streaming responses like Server-Sent Events (SSE) with Brotli, the brotli_lgwin setting determines the memory footprint per active connection. The window size is calculated as $2^{lgwin}$ bytes.

    • For high-fanout servers: Use a lower lgwin (e.g., 10 to 14) to minimize resident memory. A value of 14 (16 KiB) is the default and is generally sufficient for SSE.
    • For large, repetitive buffered bodies: Use a higher lgwin (e.g., 22) to improve the compression ratio, as the per-request cost is amortized over a single response.