URL Routing
Every app has a single urls.py with a DefaultRouter. ViewSet actions generate URL patterns automatically.
Basic Router Setup
# urls.py
from rest_framework.routers import DefaultRouter
from .views.api.agent_viewsets import AgentViewSet
from .views.api.memory_viewsets import MemoryViewSet
from .views.api.summary_viewsets import TradingSummaryViewSet
router = DefaultRouter()
router.register("agents", AgentViewSet, basename="agents")
router.register("memory", MemoryViewSet, basename="memory")
router.register("summaries", TradingSummaryViewSet, basename="summaries")
urlpatterns = router.urlsFrom agents (stockapis-trader) — three ViewSets in one router.
This generates:
/agents/ → list, create
/agents/{pk}/ → retrieve, update, destroy
/agents/{agent_id}/status/ → @action(detail=True)
/memory/ → list, create
/memory/search/ → @action(detail=False)
/summaries/ → listApp URL Registration
Apps are registered in the central URL config:
# django_cfg/apps/urls.py
urlpatterns = [
path('cfg/agents/', include('django_cfg.apps.trading.agents.urls')),
path('cfg/market/', include('django_cfg.apps.trading.market.urls')),
]Multiple Resources in One Router
When an app has several related resources, register them all in one router:
# urls.py
router = DefaultRouter()
router.register("decisions", TradeDecisionViewSet, basename="decisions")
router.register("tool-executions", ToolExecutionViewSet, basename="tool-executions")
router.register("ledger", NumericalLedgerViewSet, basename="ledger")
router.register("transitions", FSMTransitionViewSet, basename="transitions")
urlpatterns = router.urlsFrom trading (stockapis-trader) — 4 resources under one app.
Path-Parameter Actions
For hierarchical URL patterns within a single ViewSet:
@action(
detail=False,
methods=["get"],
url_path="(?P<exchange>[^/]+)/(?P<market_type>[^/]+)/symbols",
pagination_class=None,
)
def by_exchange_market(self, request, exchange, market_type):
symbols = Symbol.objects.filter(
exchange=exchange,
market_type=market_type,
status="active",
)
return Response(SymbolListSerializer(symbols, many=True).data)From symbols (stockapis) — generates /symbols/binance/linear/symbols.
Another example from the same ViewSet:
@action(
detail=False,
methods=["get"],
url_path="(?P<exchange>[^/]+)/(?P<market_type>[^/]+)/quote-assets",
)
def quote_assets(self, request, exchange, market_type):
assets = Symbol.objects.filter(
exchange=exchange, market_type=market_type
).values("quote_asset").annotate(count=Count("id"))
return Response(QuoteAssetsResponseSerializer({"assets": assets}).data)Generates /symbols/bybit/spot/quote-assets.
Custom Lookup Fields
When the URL should use a human-readable slug instead of the numeric PK:
class AgentViewSet(viewsets.ModelViewSet):
lookup_field = "agent_id" # Instead of "pk"
queryset = Agent.objects.all()Generates /agents/{agent_id}/ instead of /agents/{pk}/.
class SkillViewSet(viewsets.ModelViewSet):
lookup_field = "slug"
queryset = Skill.objects.all()Generates /skills/{slug}/ instead of /skills/{pk}/.
Nested Routers
For parent-child relationships use drf-nested-routers:
from rest_framework_nested.routers import NestedDefaultRouter
router = DefaultRouter()
router.register("workspaces", WorkspaceViewSet, basename="workspaces")
# Nested: /workspaces/{workspace_pk}/members/
workspaces_router = NestedDefaultRouter(router, "workspaces", lookup="workspace")
workspaces_router.register("members", WorkspaceMemberViewSet, basename="workspace-members")
urlpatterns = router.urls + workspaces_router.urlsURL Naming Conventions
| Pattern | Example |
|---|---|
| Resource name | router.register("symbols", ...) — plural, kebab-case |
| Basename | basename="symbols" — matches resource name |
| Action url_path | url_path="sync" — lowercase verb or noun |
| Nested path params | (?P<exchange>[^/]+) — snake_case param name |
Single Router Per App
Each app should have exactly one urls.py with one DefaultRouter:
# ✅ Correct — one router
router = DefaultRouter()
router.register("keys", APIKeyViewSet, basename="api-key")
urlpatterns = router.urls
# ❌ Wrong — multiple routers in one app
router1 = DefaultRouter()
router2 = DefaultRouter()If your app has resources that belong to different URL prefixes, consider splitting into separate apps.