mcpField
D
django-bolt

django-bolt

github.com/dj-bolt開発1.8k135 回インストール今日更新
リポジトリ

django-bolt について

Django-Bolt で API を開発するためのコーディングスキルです。開発者向けに、型付き非同期ルートの作成から認証、ドキュメント、ストリーミング、ページネーション、テストまでを案内します。

主な機能

  1. 01型付き非同期ルートハンドラー
  2. 02msgspec と Serializer によるリクエスト検証
  3. 03JWT と API キー認証
  4. 04WebSocket と SSE のレスポンス
  5. 05ページネーションと OpenAPI ドキュメント
  6. 06TestClient によるハンドラーのテスト

活用シーン

  1. 01Django-Bolt プロジェクトへのエンドポイント追加
  2. 02認証と権限の設定
  3. 03ストリーミングまたは WebSocket レスポンスの提供
  4. 04API ドキュメントの作成とテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dj-bolt/django-bolt --skill django-bolt

インストール後、Claude Code で/django-boltを入力して読み込まれたか確認します。

同梱ファイルreferences/migration-playbook.md

詳細

Django-Bolt

重要なルール

  • ユーザーが明示的に同期処理を必要としている場合を除き、常に非同期ハンドラーを使用してください
  • 非同期ハンドラーでは、非同期のデータベースアクセスを使用してください(例:Django の非同期 ORM:aget、acreate、afilter)
  • シンプルな型付きペイロードには msgspec.Struct を使用し、より高度なバリデーションや再利用可能なフィールドセットには Serializer を使用してください
  • ルートのシグネチャには型を指定してください。request.body を手動で解析するのではなく、Django-Bolt に入力を検証させてください
  • 想定される API エラーには、組み込みの HTTP 例外(HTTPException)を使用してください
  • 開発には python manage.py runbolt --dev を使用してください。uvicorn や gunicorn は使用しないでください

このスキルを適用する場合

  • ユーザーが django-bolt、BoltAPI、runbolt、Depends、response_model、APIView、JWTAuthentication、OpenAPIConfig、TestClient、WebSocket、StreamingResponse、または SSE に言及している場合
  • ユーザーが Django-Bolt アプリの起動、エンドポイントの追加、認証の接続、ドキュメントの公開、ページネーションの追加、ファイル・ストリーム・SSE イベントの返却、またはハンドラーのテストを希望している場合
  • ユーザーが FastAPI、DRF、または Django Ninja からの移行を希望している場合(references/migration-playbook.md を参照)

適用しない場合

  • ユーザーが通常の Django ビュー、テンプレート、または Django 管理サイトを扱っている場合
  • ユーザーが django-bolt に言及せず、標準的な DRF API を構築している場合
  • タスクが Django ORM のモデル、マイグレーション、または管理サイトの設定だけに関する場合

コード例

最小限のセットアップ
# settings.py
INSTALLED_APPS = [
    "django_bolt",
]
# myproject/api.py
from django_bolt import BoltAPI

api = BoltAPI()


@api.get("/health")
async def health():
    return {"status": "ok"}

実行:python manage.py runbolt --dev

BoltAPI のコンストラクターオプション
from django_bolt import (
    BoltAPI,
    CompressionConfig,
    LoggingMiddleware,
    OpenAPIConfig,
    ScalarRenderPlugin,
    TimingMiddleware,
)

api = BoltAPI(
    prefix="/api/v1",                        # URL prefix for all routes
    trailing_slash="strip",                  # "strip", "append", or "keep"
    validate_response=True,                  # validate response_model output
    compression=CompressionConfig(),         # or False to disable
    enable_logging=True,
    middleware=[                              # Python middleware stack
        TimingMiddleware,
        LoggingMiddleware,
    ],
    openapi_config=OpenAPIConfig(
        title="My API",
        version="1.0.0",
        path="/docs",
        render_plugins=[ScalarRenderPlugin()],
        enabled=True,
    ),
)
ルートデコレーターとパラメーター

すべての HTTP メソッド:@api.get、@api.post、@api.put、@api.patch、@api.delete、@api.head、@api.options。

import msgspec
from django_bolt import BoltAPI

api = BoltAPI()


class ItemOut(msgspec.Struct):
    id: int
    name: str


@api.get(
    "/items/{item_id}",
    response_model=ItemOut,        # validates and documents the output shape
    status_code=200,               # default status code
    tags=["items"],                # OpenAPI tags
    summary="Get an item",         # OpenAPI summary
    description="Fetch item by ID",
    guards=[],                     # permission guards
    auth=[],                       # auth backends
    validate_response=True,        # validate output matches response_model
)
async def get_item(item_id: int):
    return {"id": item_id, "name": "Widget"}
型付きリクエストボディ(msgspec.Struct)
import msgspec
from django_bolt import BoltAPI

api = BoltAPI()


class ItemIn(msgspec.Struct):
    name: str
    price: float
    tags: list[str] = []


class ItemOut(msgspec.Struct):
    id: int
    name: str
    price: float


@api.post("/items", response_model=ItemOut, status_code=201)
async def create_item(item: ItemIn):
    return {"id": 1, "name": item.name, "price": item.price}
バリデーション付き Serializer
from __future__ import annotations

from typing import Annotated

import msgspec
from django_bolt import BoltAPI
from django_bolt.serializers import Serializer, field_validator, model_validator

api = BoltAPI()


class UserCreate(Serializer):
    username: str
    email: str
    password: Annotated[str, msgspec.Meta(min_length=8)]

    class Config:
        write_only = {"password"}

    @field_validator("email")
    def validate_email(cls, value):
        if "@" not in value:
            raise ValueError("Invalid email")
        return value.lower()

    @model_validator
    def validate_model(self):
        if self.username.lower() == "admin":
            raise ValueError("Username 'admin' is reserved")
        return self


class UserOut(Serializer):
    id: int
    username: str
    email: str
    is_active: bool

    class Config:
        read_only = {"id"}


@api.post("/users", response_model=UserOut, status_code=201)
async def create_user(data: UserCreate):
    return {"id": 1, "username": data.username, "email": data.email, "is_active": True}
パラメーターマーカー(Query、Header、Cookie、Form、File、Path、Depends)
from __future__ import annotations

from typing import Annotated

import msgspec
from django_bolt import BoltAPI, Depends, UploadFile
from django_bolt.param_functions import Body, Cookie, File, Form, Header, Path, Query

api = BoltAPI()


# Query parameters with validation
@api.get("/search")
async def search(
    q: Annotated[str, Query(min_length=1, max_length=100)],
    page: Annotated[int, Query(ge=1)] = 1,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
    return {"query": q, "page": page, "limit": limit}


# Grouped query params via msgspec.Struct
class Filters(msgspec.Struct):
    status: str | None = None
    sort: str = "created"
    order: str = "desc"


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


# Path parameter with alias
@api.get("/users/{user_id}")
async def get_user(user_id: Annotated[int, Path(ge=1)]):
    return {"user_id": user_id}


# Header extraction
@api.get("/secure")
async def secure(x_api_key: Annotated[str, Header(alias="x-api-key")]):
    return {"key": x_api_key}


# Grouped headers via struct
class APIHeaders(msgspec.Struct):
    authorization: str
    x_request_id: str | None = None  # snake_case auto-maps to X-Request-Id


@api.get("/with-headers")
async def with_headers(headers: Annotated[APIHeaders, Header()]):
    return {"auth": headers.authorization}


# Cookie extraction
@api.get("/preferences")
async def preferences(
    theme: Annotated[str, Cookie()] = "light",
    lang: Annotated[str, Cookie()] = "en",
):
    return {"theme": theme, "lang": lang}


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


# Grouped form data via struct
class ContactForm(msgspec.Struct):
    name: str
    email: str
    message: str


@api.post("/contact")
async def contact(data: Annotated[ContactForm, Form()]):
    return {"name": data.name, "email": data.email}


# File uploads with validation
from django_bolt import FileSize

@api.post("/upload")
async def upload(
    title: Annotated[str, Form()],
    file: Annotated[
        UploadFile,
        File(max_size=FileSize.MB_30, allowed_types=["application/pdf", "image/*"]),
    ],
):
    content = await file.read()
    return {"title": title, "filename": file.filename, "size": len(content)}


# Multiple file upload
@api.post("/upload-many")
async def upload_many(
    files: Annotated[list[UploadFile], File(max_files=5)],
):
    return {"count": len(files)}


# Dependency injection
async def get_db():
    db = await connect_db()
    try:
        yield db
    finally:
        await db.close()


async def get_current_user(request):
    user_id = request.auth.get("user_id")
    return {"id": user_id, "username": "example"}


@api.get("/profile")
async def profile(
    user: Annotated[dict, Depends(get_current_user)],
):
    return {"id": user["id"], "username": user["username"]}


# Explicit JSON body marker
class UpdatePayload(msgspec.Struct):
    name: str
    value: int


@api.put("/settings")
async def update_settings(data: Annotated[UpdatePayload, Body()]):
    return {"name": data.name}
レスポンス型
from django_bolt import BoltAPI, JSON, Response, StreamingResponse
from django_bolt.responses import FileResponse, HTML, PlainText, Redirect

api = BoltAPI()


# Dict/list returns auto-serialize to JSON
@api.get("/auto-json")
async def auto_json():
    return {"message": "hello"}


# Explicit JSON with custom status
@api.post("/created")
async def created():
    return JSON({"id": 1, "created": True}, status_code=201)


# Response with custom headers and cookies
@api.post("/login")
async def login():
    return (
        Response({"token": "abc"}, status_code=200)
        .set_cookie("session", "xyz", httponly=True, secure=True, samesite="Strict")
        .set_cookie("theme", "dark", max_age=86400)
    )


@api.post("/logout")
async def logout():
    return Response({"ok": True}).delete_cookie("session")


# PlainText
@api.get("/text")
async def text():
    return PlainText("Hello, World!")


# HTML
@api.get("/page")
async def page():
    return HTML("<h1>Hello</h1><p>Welcome to the API</p>")


# Redirect
@api.get("/old-path")
async def old_path():
    return Redirect("/new-path")           # 307 temporary by default


@api.get("/moved")
async def moved():
    return Redirect("/new-location", status_code=301)  # permanent


# File download
@api.get("/download")
async def download():
    return FileResponse(
        "/path/to/report.pdf",
        filename="report.pdf",
        media_type="application/pdf",
    )


# Streaming response
@api.get("/stream")
async def stream():
    async def generate():
        for chunk in ["Hello ", "World ", "!"]:
            yield chunk

    return StreamingResponse(generate(), media_type="text/plain")
SSE(Server-Sent Events)
import asyncio
import json

from django_bolt import BoltAPI, StreamingResponse, no_compress

api = BoltAPI()


@api.get("/events")
@no_compress  # disable compression for SSE
async def events():
    async def generate():
        for i in range(10):
            data = json.dumps({"count": i, "message": f"event-{i}"})
            yield f"data: {data}\n\n"
            await asyncio.sleep(1)
        yield "event: done\ndata: stream complete\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")
WebSocket
from django_bolt import BoltAPI, WebSocket, WebSocketDisconnect

api = BoltAPI()


# Echo server
@api.websocket("/ws/echo")
async def echo(websocket: WebSocket):
    await websocket.accept()
    try:
        async for message in websocket.iter_text():
            await websocket.send_text(f"Echo: {message}")
    except WebSocketDisconnect:
        pass


# JSON messages
@api.websocket("/ws/chat")
async def chat(websocket: WebSocket, room: str):
    await websocket.accept()
    try:
        while True:
            data = await websocket.receive_json()
            response = {"room": room, "user": data.get("user"), "text": data.get("text")}
            await websocket.send_json(response)
    except WebSocketDisconnect:
        pass
認証とガード
from django_bolt import (
    AllowAny,
    APIKeyAuthentication,
    BoltAPI,
    HasAllPermissions,
    HasAnyPermission,
    HasPermission,
    IsAdminUser,
    IsAuthenticated,
    IsStaff,
    JWTAuthentication,
    Request,
    create_jwt_for_user,
)

api = BoltAPI()


# Generate JWT for a user
@api.post("/auth/login")
async def login(username: str, password: str):
    user = await verify_credentials(username, password)  # your auth logic
    if not user:
        from django_bolt.exceptions import HTTPException
        raise HTTPException(status_code=401, detail="Invalid credentials")
    token = create_jwt_for_user(user)
    return {"access_token": token}


# JWT-protected endpoint
@api.get("/me", auth=[JWTAuthentication()], guards=[IsAuthenticated()])
async def me(request: Request):
    return {"user_id": request.user.id, "username": request.user.username}


# API key auth
@api.get("/api/data", auth=[APIKeyAuthentication()], guards=[IsAuthenticated()])
async def api_data(request: Request):
    return {"data": "secret"}


# Multiple guards
@api.delete(
    "/admin/users/{user_id}",
    auth=[JWTAuthentication()],
    guards=[IsAuthenticated(), IsAdminUser()],
)
async def delete_user(user_id: int):
    await remove_user(user_id)  # your deletion logic
    return {"deleted": user_id}


# Permission-based guards
@api.post(
    "/articles",
    auth=[JWTAuthentication()],
    guards=[IsAuthenticated(), HasPermission("blog.add_article")],
)
async def create_article(title: str, body: str):
    return {"title": title}


@api.get(
    "/reports",
    auth=[JWTAuthentication()],
    guards=[IsAuthenticated(), HasAnyPermission("reports.view", "reports.export")],
)
async def view_reports():
    return {"reports": []}


@api.post(
    "/deploy",
    auth=[JWTAuthentication()],
    guards=[IsAuthenticated(), HasAllPermissions("deploy.create", "deploy.approve")],
)
async def deploy():
    return {"status": "deploying"}


# Staff-only
@api.get("/staff/dashboard", auth=[JWTAuthentication()], guards=[IsStaff()])
async def staff_dashboard():
    return {"stats": {}}


# Public endpoint (no auth required)
@api.get("/public", guards=[AllowAny()])
async def public():
    return {"public": True}
ミドルウェア(CORS、レート制限、カスタム)
from django_bolt import BoltAPI, BaseMiddleware, Request, Response, cors, middleware, no_compress, rate_limit, skip_middleware

api = BoltAPI()


# Per-route CORS
@api.get("/public/data")
@cors(
    origins=["https://example.com", "https://app.example.com"],
    methods=["GET", "POST"],
    headers=["Authorization", "Content-Type"],
    credentials=True,
    max_age=3600,
)
async def public_data():
    return {"data": "accessible cross-origin"}


# Rate limiting
@api.post("/api/expensive")
@rate_limit(rps=10, burst=20, key="ip")
async def expensive_operation():
    return {"result": "ok"}


# Skip specific middleware on a route
@api.get("/internal")
@skip_middleware("compression", "rate_limit")
async def internal():
    return {"internal": True}


# Disable compression for streaming
@api.get("/stream")
@no_compress
async def stream():
    from django_bolt import StreamingResponse
    async def gen():
        yield "chunk1"
        yield "chunk2"
    return StreamingResponse(gen(), media_type="text/plain")


# Custom Python middleware
class RequestTimingMiddleware(BaseMiddleware):
    async def process_request(self, request: Request) -> Response:
        import time
        start = time.perf_counter()
        response = await self.get_response(request)
        elapsed = time.perf_counter() - start
        response.headers["X-Response-Time"] = f"{elapsed:.4f}s"
        return response


# Attach custom middleware to a route
@api.post("/upload")
@middleware(RequestTimingMiddleware)
async def upload(request: Request):
    return {"uploaded": True}
ページネーション
from django_bolt import (
    BoltAPI,
    CursorPagination,
    LimitOffsetPagination,
    PageNumberPagination,
    paginate,
)

api = BoltAPI()


# Page-number pagination (default)
# GET /users?page=2&page_size=25
@api.get("/users")
@paginate(PageNumberPagination)
async def list_users(request):
    return await get_all_users()  # return a queryset or list


# Custom page size
class LargePages(PageNumberPagination):
    page_size = 50
    max_page_size = 200
    page_size_query_param = "page_size"


@api.get("/products")
@paginate(LargePages)
async def list_products(request):
    return await get_active_products()


# Limit-offset pagination
# GET /logs?limit=50&offset=100
@api.get("/logs")
@paginate(LimitOffsetPagination)
async def list_logs(request):
    return await get_all_logs()


# Cursor pagination (best for infinite scroll / real-time feeds)
# GET /feed?cursor=abc123
class FeedPagination(CursorPagination):
    page_size = 20
    ordering = "-created_at"


@api.get("/feed")
@paginate(FeedPagination)
async def feed(request):
    return await get_all_posts()
OpenAPI ドキュメント
from django_bolt import (
    BoltAPI,
    OpenAPIConfig,
    RapidocRenderPlugin,
    RedocRenderPlugin,
    ScalarRenderPlugin,
    StoplightRenderPlugin,
    SwaggerRenderPlugin,
)

# Minimal -- serves Swagger UI at /docs
api = BoltAPI(
    openapi_config=OpenAPIConfig(
        title="My API",
        version="1.0.0",
    ),
)

# Full configuration
api = BoltAPI(
    openapi_config=OpenAPIConfig(
        title="Acme API",
        version="2.0.0",
        description="The Acme Corp backend API",
        path="/docs",                              # docs URL
        enabled=True,
        render_plugins=[
            SwaggerRenderPlugin(),                  # /docs (default)
            RedocRenderPlugin(),                    # /docs/redoc
            ScalarRenderPlugin(),                   # /docs/scalar
            RapidocRenderPlugin(),                  # /docs/rapidoc
            StoplightRenderPlugin(),                # /docs/stoplight
        ],
        exclude_paths=["/admin", "/static"],
        include_error_responses=True,
        use_handler_docstrings=True,                # use docstrings as descriptions
    ),
)


# Per-route OpenAPI metadata
@api.get(
    "/items",
    tags=["items"],
    summary="List all items",
    description="Returns a paginated list of items with optional filters.",
)
async def list_items():
    """This docstring is also used as the description if use_handler_docstrings=True."""
    return []
エラー処理
from django_bolt import BoltAPI
from django_bolt.exceptions import HTTPException

api = BoltAPI()


@api.get("/items/{item_id}")
async def get_item(item_id: int):
    item = await fetch_item(item_id)  # your data access logic
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return item


@api.post("/items")
async def create_item(name: str, price: float):
    if price < 0:
        raise HTTPException(status_code=422, detail="Price must be positive")
    return {"id": 1, "name": name, "price": price}
テスト
from django_bolt import BoltAPI
from django_bolt.testing import TestClient

api = BoltAPI()


@api.get("/hello")
async def hello():
    return {"message": "world"}


@api.post("/items")
async def create_item(name: str, price: float):
    return {"name": name, "price": price}


# Basic usage
with TestClient(api) as client:
    # GET request
    response = client.get("/hello")
    assert response.status_code == 200
    assert response.json() == {"message": "world"}

    # POST with JSON body
    response = client.post("/items", json={"name": "Widget", "price": 9.99})
    assert response.status_code == 200
    assert response.json()["name"] == "Widget"

    # Custom headers
    response = client.get("/hello", headers={"X-Request-ID": "test-123"})
    assert response.status_code == 200

    # Test 404
    response = client.get("/nonexistent")
    assert response.status_code == 404
ルーターの構成(複数の api.py ファイル)
# users/api.py
from django_bolt import BoltAPI

api = BoltAPI(prefix="/users")

@api.get("/")
async def list_users():
    return []

@api.get("/{user_id}")
async def get_user(user_id: int):
    return {"id": user_id}
# orders/api.py
from django_bolt import BoltAPI

api = BoltAPI(prefix="/orders")

@api.get("/")
async def list_orders():
    return []

runbolt は、インストール済みアプリとプロジェクトのルートにあるすべての api.py ファイルを自動検出します。各 api = BoltAPI() インスタンスは、自動的に統合ルーターにマージされます。

ステップごとのワークフロー

ステップ 1:リクエストを分類する

コードを書く前に、タスクを次のいずれかに分類してください。

  1. 初回セットアップ -- 新しいプロジェクトまたは最初のエンドポイント
  2. 新しいエンドポイントまたは CRUD の追加 -- ルートの追加
  3. 認証または権限の設定 -- JWT、API キー、ガード
  4. ドキュメント、レスポンス、ページネーション、またはテスト -- 機能の仕上げ
  5. 移行 -- FastAPI、DRF、または Django Ninja からの移行(references/migration-playbook.md を参照)
ステップ 2:最小限の構成から始める
  1. INSTALLED_APPS に "django_bolt" を追加します
  2. プロジェクトまたはアプリのディレクトリに api.py を作成します
  3. BoltAPI() をインスタンス化します
  4. 型付きパラメーターを持つ非同期ルートハンドラーを追加します
  5. python manage.py runbolt --dev で実行します

ユーザーがすでに Django プロジェクトを持っている場合は、既存のモデル、管理サイト、アプリ、設定を維持してください。

ステップ 3:適切なプリミティブを選ぶ
必要なもの使用するもの
シンプルな JSON ボディmsgspec.Struct
高度なバリデーション/再利用可能なフィールドfield_validator を使用した Serializer
出力形式とドキュメントルートの response_model
パス/クエリ/ヘッダー/Cookie/フォーム/ファイルのパラメーターdjango_bolt.param_functions のパラメーターマーカー
認証JWTAuthentication()、APIKeyAuthentication()、またはセッション認証
権限ガード:IsAuthenticated、IsAdminUser、IsStaff、HasPermission
CRUD の構成APIView または関数ベースのルート
リアルタイム通信WebSocket を使用する @api.websocket()
一方向のライブ更新StreamingResponse(..., media_type="text/event-stream")
API ドキュメントOpenAPIConfig(デフォルトでは /docs で提供)
テストdjango_bolt.testing.TestClient
ステップ 4:短く、コピーして使える例を書く
  • 例ごとにエンドポイントを 1 つ
  • 例ごとに概念を 1 つ
  • インポートは最小限にする
  • api.py に貼り付けて、そのまま実行できるようにする

よくあるタスク

認証を追加する
  • JWT:auth=[JWTAuthentication()] を、IsAuthenticated()、IsStaff()、HasPermission("app.perm") などのガードとともに使用します
  • API キー:auth=[APIKeyAuthentication()] を使用します
  • create_jwt_for_user(user) を使用してトークンを生成します
  • ハンドラー内で request.user を通じて認証済みユーザーにアクセスします
バリデーションを追加する
  • シンプルな JSON ボディには msgspec.Struct を使用します。フィールドは自動的に検証されます
  • より詳細な検証には、@field_validator/@model_validator を備えた Serializer を使用します
  • フィールド制約(min_length、max_length、ge、le、pattern)には Annotated[type, Meta(...)] を使用します
  • パラメーターマーカー(Query、Header、Cookie、Form、File、Depends)では、検証用のキーワード引数を直接指定できます
  • request.body を手動で解析しないでください。型付きパラメーターを使用します
CRUD を追加する
  • 小規模な API では、まず関数ベースの @api.get、@api.post などを使用します
  • 複数のメソッドで同じリソースパスを共有する場合は、APIView とともに @api.view("/path") を使用します
レスポンスとストリーミングを追加する
  • JSON には dict、list、または msgspec.Struct を返します(自動的にシリアライズされます)
  • カスタムステータスコードには JSON(data, status_code=201) を使用します
  • ヘッダーと Cookie には Response(data).set_cookie(...) を使用します
  • その他のコンテンツタイプには PlainText、HTML、Redirect、FileResponse を使用します
  • SSE では、data: ...\n\n チャンクを返す StreamingResponse(generator(), media_type="text/event-stream") を使用します
  • ストリーミングエンドポイントでは @no_compress を使用します
ページネーションを追加する
  • ページベース(?page=1&page_size=20)には @paginate(PageNumberPagination) を使用します
  • オフセットベース(?limit=20&offset=40)には @paginate(LimitOffsetPagination) を使用します
  • カーソルベース(?cursor=abc)には @paginate(CursorPagination) を使用します
  • サブクラス化して page_size、max_page_size、page_size_query_param、ordering をカスタマイズします
OpenAPI ドキュメントを追加する
  • openapi_config=OpenAPIConfig(title="...", version="...") を BoltAPI() に渡します
  • ルートデコレーターで tags、summary、description、response_model を使用します
  • レンダリングプラグインを選択します:SwaggerRenderPlugin、RedocRenderPlugin、ScalarRenderPlugin など
  • ドキュメントはデフォルトで /docs で提供されます(path で変更できます)
テストを追加する
  • TestClient(api) をコンテキストマネージャーとして使用します
  • .get()、.post()、.put()、.patch()、.delete() で json=、headers=、cookies= をサポートします
  • response.status_code、.json()、.headers をアサートします
  • バリデーションエラー(422)と認証エラー(401/403)を網羅します

トラブルシューティング

「Module django_bolt not found」

重要:settings.py の INSTALLED_APPS に "django_bolt" が含まれていることを確認してください。パッケージ名は django-bolt(ハイフン)ですが、Python モジュール名は django_bolt(アンダースコア)です。

runbolt でサーバーが起動しない
  1. uvicorn や gunicorn ではなく、python manage.py runbolt --dev を使用していることを確認します
  2. Rust 拡張機能がビルドされていることを確認します。プロジェクトのルートで just build を実行します
  3. Django プロジェクトのルート、またはインストール済みアプリのディレクトリに api.py があることを確認します
ルートが見つからない(404)
  1. api.py が runbolt の探索対象ディレクトリにあることを確認します。対象はプロジェクトのルート(settings.py と同じディレクトリ)またはインストール済みアプリです
  2. api 変数が BoltAPI() インスタンスで、名前が正確に api であることを確認します
  3. ルートパスが一致していることを確認します(先頭のスラッシュが必要です:/items。items ではありません)
認証で予期せず 401 が返される
  1. JWT の場合:Authorization: Bearer <token> ヘッダーでトークンが渡されていることを確認します
  2. JWT の場合:トークンの有効期限と秘密鍵の設定を確認します
  3. セッション認証の場合:Django のセッションと認証ミドルウェアが有効になっていることを確認します
  4. ガードの場合:ユーザーオブジェクトに必要な権限/属性があることを確認します

参考資料

  • FastAPI、DRF、または Django Ninja から移行する場合は、references/migration-playbook.md を参照してください

コメント