# Django-Bolt

High-performance API framework for Django with Rust-powered HTTP handling. 60k+ RPS.

**Docs:** https://bolt.farhana.li | **Quickstart:** https://bolt.farhana.li/getting-started/quickstart

## Setup

```bash
pip install django-bolt
```

Add to `INSTALLED_APPS`:

```python
INSTALLED_APPS = ["django_bolt", ...]
```

## Auto-Discovery

Django-Bolt automatically discovers `api.py` files in your project root and all installed apps:

```
myproject/
    myproject/
        settings.py
        api.py          # Auto-discovered (project-level)
    users/
        api.py          # Auto-discovered (app in INSTALLED_APPS)
```

**APIs in non-discoverable locations:** If your API is in a file that's not auto-discovered (e.g., `myproject/views.py`), import and mount it from a discoverable `api.py`:

```python
# myproject/api.py (auto-discovered)
from django_bolt import BoltAPI
from myproject.views import internal_api

api = BoltAPI()
api.mount("/internal", internal_api)
```

Run with: `python manage.py runbolt --dev`

## Basic Routes

> Details: https://bolt.farhana.li/topics/routing

```python
from django_bolt import BoltAPI

api = BoltAPI()

@api.get("/")
async def root():
    return {"message": "Hello"}

@api.get("/items/{item_id}")
async def get_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}
```

Path params use `{name}` syntax and auto-convert to the annotated type. Query params are inferred from function args not in the path.

## Request Body & Validation

> Details: https://bolt.farhana.li/topics/requests | https://bolt.farhana.li/topics/serializers

Use `Serializer` with `msgspec.Meta` for validated request bodies:

```python
from typing import Annotated
from msgspec import Meta
from django_bolt.serializers import Serializer, field_validator

class CreateUser(Serializer):
    name: Annotated[str, Meta(min_length=2, max_length=100)]
    email: Annotated[str, Meta(pattern=r"^[^@]+@[^@]+$")]
    age: Annotated[int, Meta(ge=0, le=150)] = 18

    @field_validator("name")
    def validate_name(cls, value):
        if value.lower() == "admin":
            raise ValueError("Reserved name")
        return value

@api.post("/users")
async def create_user(user: CreateUser):
    return {"created": user.name}
```

## Query Parameter Models

Group query params into a validated `Serializer`:

```python
from django_bolt.param_functions import Query

class Filters(Serializer):
    limit: Annotated[int, Meta(ge=1, le=100)] = 10
    search: str | None = None

@api.get("/items")
async def list_items(filters: Annotated[Filters, Query()]):
    return {"limit": filters.limit, "search": filters.search}
```

## Headers, Cookies, Form, File

```python
from django_bolt.param_functions import Header, Cookie, Form, File

@api.get("/auth")
async def auth(token: Annotated[str, Header(alias="Authorization")]):
    return {"token": token}

@api.post("/login")
async def login(
    username: Annotated[str, Form()],
    password: Annotated[str, Form()],
):
    return {"username": username}

@api.post("/upload")
async def upload(files: Annotated[list[dict], File(alias="file")]):
    return {"count": len(files)}
```

## Mount & Router

### Mount (Sub-API with own middleware)

Import an API from another file and mount it under a prefix. The sub-API keeps its own middleware.

```python
# users/api.py
from django_bolt import BoltAPI

api = BoltAPI(middleware=[AuthMiddleware])

@api.get("/me")
async def me(request):
    return {"user": request.user.username}
```

```python
# myproject/api.py
from django_bolt import BoltAPI
from users.api import api as users_api

api = BoltAPI()
api.mount("/users", users_api)  # Routes: /users/me
```

## Authentication

> Details: https://bolt.farhana.li/topics/authentication

Configure globally in `settings.py` so you don't repeat on every endpoint:

```python
# settings.py
from django_bolt.auth import JWTAuthentication, IsAuthenticated

BOLT_AUTHENTICATION_CLASSES = [JWTAuthentication()]
BOLT_DEFAULT_PERMISSION_CLASSES = [IsAuthenticated()]
```

Now all endpoints require auth by default:

```python
@api.get("/profile")  # Automatically requires JWT + IsAuthenticated
async def profile(request):
    return {"user_id": request.user.id}

# Override for public endpoints
from django_bolt.auth import AllowAny

@api.post("/login", guards=[AllowAny()])  # No auth required
async def login(creds: LoginRequest):
    user = await aauthenticate(username=creds.username, password=creds.password)
    if not user:
        raise Unauthorized("Invalid credentials")
    return {"access_token": create_jwt_for_user(user, expires_in=3600)}
```

## Guards (Permissions)

> Details: https://bolt.farhana.li/topics/permissions

Guards run in Rust after auth, before your handler. Permission data must be in the JWT token.

```python
from django_bolt.auth import IsAdminUser, IsStaff, HasPermission, HasAnyPermission

@api.get("/admin", guards=[IsAdminUser()])
async def admin():
    return {"admin": True}

@api.get("/articles", guards=[HasPermission("blog.view_article")])
async def articles():
    return []
```

Include permissions when creating tokens:

```python
token = create_jwt_for_user(user, extra_claims={"permissions": list(user.get_all_permissions())})
```

## Middleware

> Details: https://bolt.farhana.li/topics/middleware

### Django Middleware Integration

```python
api = BoltAPI(django_middleware=True)  # Load from settings.MIDDLEWARE
```

### Custom Middleware

```python
class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    async def __call__(self, request):
        import time
        start = time.time()
        response = await self.get_response(request)
        response.headers["X-Response-Time"] = f"{time.time() - start:.4f}s"
        return response

api = BoltAPI(middleware=[TimingMiddleware])
```

### Decorator Middleware (Rust-accelerated)

```python
from django_bolt.middleware import rate_limit, cors, no_compress

@api.get("/data")
@rate_limit(rps=100, burst=200)
async def data():
    return {"data": "value"}

@api.get("/public")
@cors(origins=["https://example.com"], credentials=True)
async def public():
    return {}
```

## Responses

> Details: https://bolt.farhana.li/topics/responses

```python
from django_bolt.responses import PlainText, HTML, Redirect, FileResponse, StreamingResponse
from django_bolt.shortcuts import render

@api.get("/text")
async def text():
    return PlainText("Hello")

@api.get("/page")
async def page(request):
    return render(request, "myapp/page.html", {"title": "Hello"})

@api.get("/download")
async def download():
    return FileResponse("/path/file.pdf", filename="doc.pdf")

@api.get("/stream")
@no_compress
async def stream():
    async def gen():
        for i in range(10):
            yield f"data: {i}\n\n"
    return StreamingResponse(gen(), media_type="text/event-stream")
```

## WebSockets

> Details: https://bolt.farhana.li/topics/websocket

```python
from django_bolt import WebSocket

@api.websocket("/ws")
async def ws(websocket: WebSocket):
    await websocket.accept()
    async for msg in websocket.iter_text():
        await websocket.send_text(f"Echo: {msg}")
```

## Error Handling

> Details: https://bolt.farhana.li/topics/error-handling

```python
from django_bolt.exceptions import HTTPException, NotFound, BadRequest, Unauthorized, Forbidden

@api.get("/item/{id}")
async def get_item(id: int):
    item = await Item.objects.filter(id=id).afirst()
    if not item:
        raise NotFound(f"Item {id} not found")
    return item
```

## OpenAPI Docs

> Details: https://bolt.farhana.li/topics/openapi

Auto-generated at `/docs` (Swagger), `/docs/redoc`, `/docs/scalar`.

```python
api = BoltAPI(title="My API", version="1.0.0")

@api.get("/items", tags=["items"], summary="List items")
async def list_items():
    return []
```

## Global Defaults (settings.py)

> Details: https://bolt.farhana.li/ref/settings

```python
from django_bolt.auth import JWTAuthentication, IsAuthenticated

BOLT_AUTHENTICATION_CLASSES = [JWTAuthentication()]
BOLT_DEFAULT_PERMISSION_CLASSES = [IsAuthenticated()]  # Default: AllowAny
CORS_ALLOWED_ORIGINS = ["https://example.com"]
BOLT_ALLOWED_FILE_PATHS = ["/var/www/static"]
```
