django-bolt について
Django-Bolt で API を開発するためのコーディングスキルです。開発者向けに、型付き非同期ルートの作成から認証、ドキュメント、ストリーミング、ページネーション、テストまでを案内します。
主な機能
- 01型付き非同期ルートハンドラー
- 02msgspec と Serializer によるリクエスト検証
- 03JWT と API キー認証
- 04WebSocket と SSE のレスポンス
- 05ページネーションと OpenAPI ドキュメント
- 06TestClient によるハンドラーのテスト
活用シーン
- 01Django-Bolt プロジェクトへのエンドポイント追加
- 02認証と権限の設定
- 03ストリーミングまたは WebSocket レスポンスの提供
- 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:リクエストを分類する
コードを書く前に、タスクを次のいずれかに分類してください。
- 初回セットアップ -- 新しいプロジェクトまたは最初のエンドポイント
- 新しいエンドポイントまたは CRUD の追加 -- ルートの追加
- 認証または権限の設定 -- JWT、API キー、ガード
- ドキュメント、レスポンス、ページネーション、またはテスト -- 機能の仕上げ
- 移行 -- FastAPI、DRF、または Django Ninja からの移行(
references/migration-playbook.mdを参照)
ステップ 2:最小限の構成から始める
INSTALLED_APPSに"django_bolt"を追加します- プロジェクトまたはアプリのディレクトリに
api.pyを作成します BoltAPI()をインスタンス化します- 型付きパラメーターを持つ非同期ルートハンドラーを追加します
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 でサーバーが起動しない
uvicornやgunicornではなく、python manage.py runbolt --devを使用していることを確認します- Rust 拡張機能がビルドされていることを確認します。プロジェクトのルートで
just buildを実行します - Django プロジェクトのルート、またはインストール済みアプリのディレクトリに
api.pyがあることを確認します
ルートが見つからない(404)
api.pyがrunboltの探索対象ディレクトリにあることを確認します。対象はプロジェクトのルート(settings.pyと同じディレクトリ)またはインストール済みアプリですapi変数がBoltAPI()インスタンスで、名前が正確にapiであることを確認します- ルートパスが一致していることを確認します(先頭のスラッシュが必要です:
/items。itemsではありません)
認証で予期せず 401 が返される
- JWT の場合:
Authorization: Bearer <token>ヘッダーでトークンが渡されていることを確認します - JWT の場合:トークンの有効期限と秘密鍵の設定を確認します
- セッション認証の場合:Django のセッションと認証ミドルウェアが有効になっていることを確認します
- ガードの場合:ユーザーオブジェクトに必要な権限/属性があることを確認します
参考資料
- FastAPI、DRF、または Django Ninja から移行する場合は、
references/migration-playbook.mdを参照してください
コメント
Markdown 対応