PEP 484 type stubs for Django REST Framework enabling static type checking with comprehensive type definitions for all major DRF components
—
Django REST Framework routers provide automatic URL routing for ViewSets, enabling RESTful API endpoints with minimal configuration. The type stubs ensure type-safe URL pattern generation and ViewSet registration.
class BaseRouter:
"""Base class for all DRF routers."""
registry: list[tuple[str, type[ViewSetMixin], str]]
def __init__(self) -> None: ...
def register(
self,
prefix: str,
viewset: type[ViewSetMixin],
basename: str | None = None
) -> None:
"""
Register a viewset with the router.
Args:
prefix: URL prefix for the viewset routes
viewset: ViewSet class to register
basename: Base name for URL patterns (auto-generated if None)
"""
...
def get_urls(self) -> list[URLPattern]:
"""
Generate URL patterns for all registered viewsets.
Returns:
list[URLPattern]: Django URL patterns
"""
...
def get_api_root_view(self, api_urls: list[URLPattern] | None = None) -> Callable: ...Parameters:
prefix: str - URL prefix (e.g., 'books', 'api/v1/authors')viewset: type[ViewSetMixin] - ViewSet class to registerbasename: str | None - Base name for URL reversing (auto-detected if None)class SimpleRouter(BaseRouter):
"""Simple router that generates standard RESTful routes."""
routes: list[Route]
def __init__(self, trailing_slash: bool = True) -> None: ...
def get_default_basename(self, viewset: type[ViewSetMixin]) -> str: ...
def get_lookup_regex(
self,
viewset: type[ViewSetMixin],
lookup_prefix: str = ''
) -> str: ...Parameters:
trailing_slash: bool - Add trailing slash to URLs (default: True)class DefaultRouter(SimpleRouter):
"""Router that includes an API root view and browsable API."""
include_root_view: bool
include_format_suffixes: bool
root_view_name: str
def __init__(
self,
trailing_slash: bool = True,
include_root_view: bool = True,
include_format_suffixes: bool = True
) -> None: ...
def get_api_root_view(self, api_urls: list[URLPattern] | None = None) -> type[APIRootView]: ...Parameters:
include_root_view: bool - Include API root endpoint (default: True)include_format_suffixes: bool - Support format suffixes like .json (default: True)root_view_name: str - Name for the root viewclass Route(NamedTuple):
"""Configuration for a standard route."""
url: str
mapping: dict[str, str]
name: str
detail: bool
initkwargs: dict[str, Any]Fields:
url: str - URL pattern with format placeholdersmapping: dict[str, str] - HTTP methods to ViewSet actions mappingname: str - URL name patterndetail: bool - Whether this is a detail route (requires object lookup)initkwargs: dict[str, Any] - Additional ViewSet initialization kwargsclass DynamicRoute(NamedTuple):
"""Configuration for custom @action routes."""
url: str
name: str
detail: bool
initkwargs: dict[str, Any]class APIRootView(APIView):
"""Default API root view that lists available endpoints."""
_ignore_model_permissions: bool
schema: None
api_root_dict: dict[str, str] | None
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...from rest_framework.routers import DefaultRouter
from rest_framework import viewsets
# Create router instance
router = DefaultRouter()
# Register viewsets
router.register(r'books', BookViewSet)
router.register(r'authors', AuthorViewSet)
router.register(r'categories', CategoryViewSet, basename='category')
# Get URL patterns
urlpatterns = router.urls
# Or include in main URLconf
from django.urls import path, include
urlpatterns = [
path('api/', include(router.urls)),
path('admin/', admin.site.urls),
]# For BookViewSet registration, router generates:
# GET /books/ -> BookViewSet.list()
# POST /books/ -> BookViewSet.create()
# GET /books/{id}/ -> BookViewSet.retrieve()
# PUT /books/{id}/ -> BookViewSet.update()
# PATCH /books/{id}/ -> BookViewSet.partial_update()
# DELETE /books/{id}/ -> BookViewSet.destroy()
# URL names for reversing:
# book-list
# book-detailfrom django.contrib.auth.models import User
class UserViewSet(viewsets.ModelViewSet[User]):
def get_queryset(self) -> QuerySet[User]:
# Custom queryset logic
return User.objects.filter(is_active=True)
# Register with custom basename since queryset is dynamic
router.register(r'users', UserViewSet, basename='user')
# Generated URL names: user-list, user-detail# API v1 router
v1_router = DefaultRouter()
v1_router.register(r'books', v1.BookViewSet)
v1_router.register(r'authors', v1.AuthorViewSet)
# API v2 router
v2_router = DefaultRouter()
v2_router.register(r'books', v2.BookViewSet)
v2_router.register(r'authors', v2.AuthorViewSet)
v2_router.register(r'reviews', v2.ReviewViewSet)
# URL configuration
urlpatterns = [
path('api/v1/', include((v1_router.urls, 'v1'), namespace='v1')),
path('api/v2/', include((v2_router.urls, 'v2'), namespace='v2')),
]class BookViewSet(viewsets.ModelViewSet[Book]):
"""Standard ModelViewSet with default actions."""
queryset = Book.objects.all()
serializer_class = BookSerializer
# These methods are automatically routed:
# list() -> GET /books/
# create() -> POST /books/
# retrieve() -> GET /books/{id}/
# update() -> PUT /books/{id}/
# partial_update() -> PATCH /books/{id}/
# destroy() -> DELETE /books/{id}/from rest_framework.decorators import action
from rest_framework.response import Response
class BookViewSet(viewsets.ModelViewSet[Book]):
queryset = Book.objects.all()
serializer_class = BookSerializer
@action(detail=False, methods=['get'])
def popular(self, request: Request) -> Response:
"""List popular books - generates GET /books/popular/"""
popular_books = Book.objects.filter(rating__gte=4.0)
serializer = self.get_serializer(popular_books, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post', 'delete'])
def favorite(self, request: Request, pk: str | None = None) -> Response:
"""Add/remove favorite - generates POST/DELETE /books/{id}/favorite/"""
book = self.get_object()
if request.method == 'POST':
request.user.favorite_books.add(book)
return Response({'status': 'favorited'})
else: # DELETE
request.user.favorite_books.remove(book)
return Response({'status': 'unfavorited'})
@action(
detail=False,
methods=['get'],
url_path='by-author/(?P<author_id>[^/.]+)',
url_name='by_author'
)
def books_by_author(self, request: Request, author_id: str | None = None) -> Response:
"""Custom URL pattern - generates GET /books/by-author/{author_id}/"""
books = Book.objects.filter(author_id=author_id)
serializer = self.get_serializer(books, many=True)
return Response(serializer.data)Action Parameters:
detail: bool - Whether action requires object lookup (True) or collection-level (False)methods: list[str] - HTTP methods supported by the actionurl_path: str - Custom URL path segment (defaults to method name)url_name: str - Custom name for URL reversing (defaults to method name)def escape_curly_brackets(url_path: str) -> str:
"""Escape curly brackets in URL paths for regex compilation."""
...
def flatten(list_of_lists: Iterable[Iterable[Any]]) -> Iterable[Any]:
"""Flatten nested iterables."""
...from django.urls import reverse
# Reverse standard ViewSet URLs
book_list_url = reverse('book-list') # /api/books/
book_detail_url = reverse('book-detail', kwargs={'pk': 1}) # /api/books/1/
# Reverse custom action URLs
popular_books_url = reverse('book-popular') # /api/books/popular/
favorite_url = reverse('book-favorite', kwargs={'pk': 1}) # /api/books/1/favorite/
by_author_url = reverse('book-by-author', kwargs={'author_id': 5}) # /api/books/by-author/5/
# With namespaces
v1_books_url = reverse('v1:book-list')
v2_books_url = reverse('v2:book-list')from rest_framework.routers import Route, DynamicRoute
class CustomRouter(SimpleRouter):
"""Router with custom route patterns."""
routes = [
# List route
Route(
url=r'^{prefix}/$',
mapping={'get': 'list', 'post': 'create'},
name='{basename}-list',
detail=False,
initkwargs={'suffix': 'List'}
),
# Detail route
Route(
url=r'^{prefix}/{lookup}/$',
mapping={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
detail=True,
initkwargs={'suffix': 'Instance'}
),
# Custom action route
DynamicRoute(
url=r'^{prefix}/{lookup}/{url_path}/$',
name='{basename}-{url_name}',
detail=True,
initkwargs={}
),
]from rest_framework_nested import routers
# Parent router
router = routers.SimpleRouter()
router.register(r'authors', AuthorViewSet, basename='author')
# Nested router
authors_router = routers.NestedSimpleRouter(router, r'authors', lookup='author')
authors_router.register(r'books', BookViewSet, basename='author-books')
# Generated URLs:
# /authors/ -> AuthorViewSet.list()
# /authors/{id}/ -> AuthorViewSet.retrieve()
# /authors/{id}/books/ -> BookViewSet.list() (filtered by author)
# /authors/{id}/books/{id}/ -> BookViewSet.retrieve()class BookViewSet(viewsets.ModelViewSet[Book]):
"""ViewSet with custom lookup field."""
queryset = Book.objects.all()
serializer_class = BookSerializer
lookup_field = 'isbn' # Use ISBN instead of primary key
lookup_url_kwarg = 'isbn'
# Router generates URLs like: /books/978-0123456789/
router.register(r'books', BookViewSet)from django.conf import settings
router = DefaultRouter()
# Always register these
router.register(r'books', BookViewSet)
router.register(r'authors', AuthorViewSet)
# Conditionally register admin endpoints
if settings.DEBUG or settings.ENABLE_ADMIN_API:
router.register(r'admin/users', AdminUserViewSet, basename='admin-user')
router.register(r'admin/stats', AdminStatsViewSet, basename='admin-stats')from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
class RouterURLTests(APITestCase):
"""Test router-generated URL patterns."""
def test_book_list_url(self) -> None:
"""Test book list URL resolution."""
url = reverse('book-list')
self.assertEqual(url, '/api/books/')
def test_book_detail_url(self) -> None:
"""Test book detail URL resolution."""
url = reverse('book-detail', kwargs={'pk': 1})
self.assertEqual(url, '/api/books/1/')
def test_custom_action_url(self) -> None:
"""Test custom action URL resolution."""
url = reverse('book-popular')
self.assertEqual(url, '/api/books/popular/')
def test_nested_action_url(self) -> None:
"""Test nested action URL resolution."""
url = reverse('book-favorite', kwargs={'pk': 1})
self.assertEqual(url, '/api/books/1/favorite/')class BookViewSetTests(APITestCase):
"""Test ViewSet actions through router URLs."""
def setUp(self) -> None:
self.book = Book.objects.create(title='Test Book', author='Test Author')
def test_list_action(self) -> None:
"""Test list action via router URL."""
url = reverse('book-list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_detail_action(self) -> None:
"""Test retrieve action via router URL."""
url = reverse('book-detail', kwargs={'pk': self.book.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_custom_action(self) -> None:
"""Test custom action via router URL."""
url = reverse('book-popular')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)# Use SimpleRouter for better performance when root view not needed
router = SimpleRouter(trailing_slash=False) # Also saves URL matching
# Minimize registered viewsets
router.register(r'books', BookViewSet)
# Don't register unused viewsets
# Use basename when queryset is dynamic to avoid inspection
router.register(r'user-books', UserBookViewSet, basename='user-book')from django.core.cache import cache
class CachedRouter(DefaultRouter):
"""Router with URL caching."""
def get_urls(self) -> list[URLPattern]:
cache_key = f"router_urls_{len(self.registry)}"
urls = cache.get(cache_key)
if urls is None:
urls = super().get_urls()
cache.set(cache_key, urls, timeout=3600) # Cache for 1 hour
return urlsThis comprehensive routing system provides type-safe URL pattern generation and ViewSet registration, enabling confident API endpoint configuration with full mypy integration and automatic RESTful URL structure.
Install with Tessl CLI
npx tessl i tessl/pypi-djangorestframework-stubs