0
# Routers & URLs
1
2
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.
3
4
## Router Classes
5
6
### BaseRouter
7
8
```python { .api }
9
class BaseRouter:
10
"""Base class for all DRF routers."""
11
12
registry: list[tuple[str, type[ViewSetMixin], str]]
13
14
def __init__(self) -> None: ...
15
16
def register(
17
self,
18
prefix: str,
19
viewset: type[ViewSetMixin],
20
basename: str | None = None
21
) -> None:
22
"""
23
Register a viewset with the router.
24
25
Args:
26
prefix: URL prefix for the viewset routes
27
viewset: ViewSet class to register
28
basename: Base name for URL patterns (auto-generated if None)
29
"""
30
...
31
32
def get_urls(self) -> list[URLPattern]:
33
"""
34
Generate URL patterns for all registered viewsets.
35
36
Returns:
37
list[URLPattern]: Django URL patterns
38
"""
39
...
40
41
def get_api_root_view(self, api_urls: list[URLPattern] | None = None) -> Callable: ...
42
```
43
44
**Parameters:**
45
- `prefix: str` - URL prefix (e.g., 'books', 'api/v1/authors')
46
- `viewset: type[ViewSetMixin]` - ViewSet class to register
47
- `basename: str | None` - Base name for URL reversing (auto-detected if None)
48
49
### SimpleRouter
50
51
```python { .api }
52
class SimpleRouter(BaseRouter):
53
"""Simple router that generates standard RESTful routes."""
54
55
routes: list[Route]
56
57
def __init__(self, trailing_slash: bool = True) -> None: ...
58
59
def get_default_basename(self, viewset: type[ViewSetMixin]) -> str: ...
60
def get_lookup_regex(
61
self,
62
viewset: type[ViewSetMixin],
63
lookup_prefix: str = ''
64
) -> str: ...
65
```
66
67
**Parameters:**
68
- `trailing_slash: bool` - Add trailing slash to URLs (default: True)
69
70
### DefaultRouter
71
72
```python { .api }
73
class DefaultRouter(SimpleRouter):
74
"""Router that includes an API root view and browsable API."""
75
76
include_root_view: bool
77
include_format_suffixes: bool
78
root_view_name: str
79
80
def __init__(
81
self,
82
trailing_slash: bool = True,
83
include_root_view: bool = True,
84
include_format_suffixes: bool = True
85
) -> None: ...
86
87
def get_api_root_view(self, api_urls: list[URLPattern] | None = None) -> type[APIRootView]: ...
88
```
89
90
**Parameters:**
91
- `include_root_view: bool` - Include API root endpoint (default: True)
92
- `include_format_suffixes: bool` - Support format suffixes like .json (default: True)
93
- `root_view_name: str` - Name for the root view
94
95
## Route Configuration
96
97
### Route
98
99
```python { .api }
100
class Route(NamedTuple):
101
"""Configuration for a standard route."""
102
103
url: str
104
mapping: dict[str, str]
105
name: str
106
detail: bool
107
initkwargs: dict[str, Any]
108
```
109
110
**Fields:**
111
- `url: str` - URL pattern with format placeholders
112
- `mapping: dict[str, str]` - HTTP methods to ViewSet actions mapping
113
- `name: str` - URL name pattern
114
- `detail: bool` - Whether this is a detail route (requires object lookup)
115
- `initkwargs: dict[str, Any]` - Additional ViewSet initialization kwargs
116
117
### DynamicRoute
118
119
```python { .api }
120
class DynamicRoute(NamedTuple):
121
"""Configuration for custom @action routes."""
122
123
url: str
124
name: str
125
detail: bool
126
initkwargs: dict[str, Any]
127
```
128
129
### APIRootView
130
131
```python { .api }
132
class APIRootView(APIView):
133
"""Default API root view that lists available endpoints."""
134
135
_ignore_model_permissions: bool
136
schema: None
137
api_root_dict: dict[str, str] | None
138
139
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
140
```
141
142
## Router Usage Examples
143
144
### Basic Router Setup
145
146
```python { .api }
147
from rest_framework.routers import DefaultRouter
148
from rest_framework import viewsets
149
150
# Create router instance
151
router = DefaultRouter()
152
153
# Register viewsets
154
router.register(r'books', BookViewSet)
155
router.register(r'authors', AuthorViewSet)
156
router.register(r'categories', CategoryViewSet, basename='category')
157
158
# Get URL patterns
159
urlpatterns = router.urls
160
161
# Or include in main URLconf
162
from django.urls import path, include
163
164
urlpatterns = [
165
path('api/', include(router.urls)),
166
path('admin/', admin.site.urls),
167
]
168
```
169
170
### Generated URL Patterns
171
172
```python { .api }
173
# For BookViewSet registration, router generates:
174
# GET /books/ -> BookViewSet.list()
175
# POST /books/ -> BookViewSet.create()
176
# GET /books/{id}/ -> BookViewSet.retrieve()
177
# PUT /books/{id}/ -> BookViewSet.update()
178
# PATCH /books/{id}/ -> BookViewSet.partial_update()
179
# DELETE /books/{id}/ -> BookViewSet.destroy()
180
181
# URL names for reversing:
182
# book-list
183
# book-detail
184
```
185
186
### Custom Basename
187
188
```python { .api }
189
from django.contrib.auth.models import User
190
191
class UserViewSet(viewsets.ModelViewSet[User]):
192
def get_queryset(self) -> QuerySet[User]:
193
# Custom queryset logic
194
return User.objects.filter(is_active=True)
195
196
# Register with custom basename since queryset is dynamic
197
router.register(r'users', UserViewSet, basename='user')
198
199
# Generated URL names: user-list, user-detail
200
```
201
202
### Multiple Router Configuration
203
204
```python { .api }
205
# API v1 router
206
v1_router = DefaultRouter()
207
v1_router.register(r'books', v1.BookViewSet)
208
v1_router.register(r'authors', v1.AuthorViewSet)
209
210
# API v2 router
211
v2_router = DefaultRouter()
212
v2_router.register(r'books', v2.BookViewSet)
213
v2_router.register(r'authors', v2.AuthorViewSet)
214
v2_router.register(r'reviews', v2.ReviewViewSet)
215
216
# URL configuration
217
urlpatterns = [
218
path('api/v1/', include((v1_router.urls, 'v1'), namespace='v1')),
219
path('api/v2/', include((v2_router.urls, 'v2'), namespace='v2')),
220
]
221
```
222
223
## ViewSet Actions & Routing
224
225
### Standard Actions
226
227
```python { .api }
228
class BookViewSet(viewsets.ModelViewSet[Book]):
229
"""Standard ModelViewSet with default actions."""
230
231
queryset = Book.objects.all()
232
serializer_class = BookSerializer
233
234
# These methods are automatically routed:
235
# list() -> GET /books/
236
# create() -> POST /books/
237
# retrieve() -> GET /books/{id}/
238
# update() -> PUT /books/{id}/
239
# partial_update() -> PATCH /books/{id}/
240
# destroy() -> DELETE /books/{id}/
241
```
242
243
### Custom Actions
244
245
```python { .api }
246
from rest_framework.decorators import action
247
from rest_framework.response import Response
248
249
class BookViewSet(viewsets.ModelViewSet[Book]):
250
queryset = Book.objects.all()
251
serializer_class = BookSerializer
252
253
@action(detail=False, methods=['get'])
254
def popular(self, request: Request) -> Response:
255
"""List popular books - generates GET /books/popular/"""
256
popular_books = Book.objects.filter(rating__gte=4.0)
257
serializer = self.get_serializer(popular_books, many=True)
258
return Response(serializer.data)
259
260
@action(detail=True, methods=['post', 'delete'])
261
def favorite(self, request: Request, pk: str | None = None) -> Response:
262
"""Add/remove favorite - generates POST/DELETE /books/{id}/favorite/"""
263
book = self.get_object()
264
265
if request.method == 'POST':
266
request.user.favorite_books.add(book)
267
return Response({'status': 'favorited'})
268
else: # DELETE
269
request.user.favorite_books.remove(book)
270
return Response({'status': 'unfavorited'})
271
272
@action(
273
detail=False,
274
methods=['get'],
275
url_path='by-author/(?P<author_id>[^/.]+)',
276
url_name='by_author'
277
)
278
def books_by_author(self, request: Request, author_id: str | None = None) -> Response:
279
"""Custom URL pattern - generates GET /books/by-author/{author_id}/"""
280
books = Book.objects.filter(author_id=author_id)
281
serializer = self.get_serializer(books, many=True)
282
return Response(serializer.data)
283
```
284
285
**Action Parameters:**
286
- `detail: bool` - Whether action requires object lookup (True) or collection-level (False)
287
- `methods: list[str]` - HTTP methods supported by the action
288
- `url_path: str` - Custom URL path segment (defaults to method name)
289
- `url_name: str` - Custom name for URL reversing (defaults to method name)
290
291
## URL Utilities
292
293
### Helper Functions
294
295
```python { .api }
296
def escape_curly_brackets(url_path: str) -> str:
297
"""Escape curly brackets in URL paths for regex compilation."""
298
...
299
300
def flatten(list_of_lists: Iterable[Iterable[Any]]) -> Iterable[Any]:
301
"""Flatten nested iterables."""
302
...
303
```
304
305
### URL Reversing
306
307
```python { .api }
308
from django.urls import reverse
309
310
# Reverse standard ViewSet URLs
311
book_list_url = reverse('book-list') # /api/books/
312
book_detail_url = reverse('book-detail', kwargs={'pk': 1}) # /api/books/1/
313
314
# Reverse custom action URLs
315
popular_books_url = reverse('book-popular') # /api/books/popular/
316
favorite_url = reverse('book-favorite', kwargs={'pk': 1}) # /api/books/1/favorite/
317
by_author_url = reverse('book-by-author', kwargs={'author_id': 5}) # /api/books/by-author/5/
318
319
# With namespaces
320
v1_books_url = reverse('v1:book-list')
321
v2_books_url = reverse('v2:book-list')
322
```
323
324
## Advanced Router Configuration
325
326
### Custom Route Patterns
327
328
```python { .api }
329
from rest_framework.routers import Route, DynamicRoute
330
331
class CustomRouter(SimpleRouter):
332
"""Router with custom route patterns."""
333
334
routes = [
335
# List route
336
Route(
337
url=r'^{prefix}/$',
338
mapping={'get': 'list', 'post': 'create'},
339
name='{basename}-list',
340
detail=False,
341
initkwargs={'suffix': 'List'}
342
),
343
# Detail route
344
Route(
345
url=r'^{prefix}/{lookup}/$',
346
mapping={
347
'get': 'retrieve',
348
'put': 'update',
349
'patch': 'partial_update',
350
'delete': 'destroy'
351
},
352
name='{basename}-detail',
353
detail=True,
354
initkwargs={'suffix': 'Instance'}
355
),
356
# Custom action route
357
DynamicRoute(
358
url=r'^{prefix}/{lookup}/{url_path}/$',
359
name='{basename}-{url_name}',
360
detail=True,
361
initkwargs={}
362
),
363
]
364
```
365
366
### Nested Routes
367
368
```python { .api }
369
from rest_framework_nested import routers
370
371
# Parent router
372
router = routers.SimpleRouter()
373
router.register(r'authors', AuthorViewSet, basename='author')
374
375
# Nested router
376
authors_router = routers.NestedSimpleRouter(router, r'authors', lookup='author')
377
authors_router.register(r'books', BookViewSet, basename='author-books')
378
379
# Generated URLs:
380
# /authors/ -> AuthorViewSet.list()
381
# /authors/{id}/ -> AuthorViewSet.retrieve()
382
# /authors/{id}/books/ -> BookViewSet.list() (filtered by author)
383
# /authors/{id}/books/{id}/ -> BookViewSet.retrieve()
384
```
385
386
### Router with Custom Lookup
387
388
```python { .api }
389
class BookViewSet(viewsets.ModelViewSet[Book]):
390
"""ViewSet with custom lookup field."""
391
392
queryset = Book.objects.all()
393
serializer_class = BookSerializer
394
lookup_field = 'isbn' # Use ISBN instead of primary key
395
lookup_url_kwarg = 'isbn'
396
397
# Router generates URLs like: /books/978-0123456789/
398
router.register(r'books', BookViewSet)
399
```
400
401
### Conditional Registration
402
403
```python { .api }
404
from django.conf import settings
405
406
router = DefaultRouter()
407
408
# Always register these
409
router.register(r'books', BookViewSet)
410
router.register(r'authors', AuthorViewSet)
411
412
# Conditionally register admin endpoints
413
if settings.DEBUG or settings.ENABLE_ADMIN_API:
414
router.register(r'admin/users', AdminUserViewSet, basename='admin-user')
415
router.register(r'admin/stats', AdminStatsViewSet, basename='admin-stats')
416
```
417
418
## Testing Router URLs
419
420
### URL Pattern Testing
421
422
```python { .api }
423
from django.test import TestCase
424
from django.urls import reverse
425
from rest_framework.test import APITestCase
426
427
class RouterURLTests(APITestCase):
428
"""Test router-generated URL patterns."""
429
430
def test_book_list_url(self) -> None:
431
"""Test book list URL resolution."""
432
url = reverse('book-list')
433
self.assertEqual(url, '/api/books/')
434
435
def test_book_detail_url(self) -> None:
436
"""Test book detail URL resolution."""
437
url = reverse('book-detail', kwargs={'pk': 1})
438
self.assertEqual(url, '/api/books/1/')
439
440
def test_custom_action_url(self) -> None:
441
"""Test custom action URL resolution."""
442
url = reverse('book-popular')
443
self.assertEqual(url, '/api/books/popular/')
444
445
def test_nested_action_url(self) -> None:
446
"""Test nested action URL resolution."""
447
url = reverse('book-favorite', kwargs={'pk': 1})
448
self.assertEqual(url, '/api/books/1/favorite/')
449
```
450
451
### ViewSet Action Testing
452
453
```python { .api }
454
class BookViewSetTests(APITestCase):
455
"""Test ViewSet actions through router URLs."""
456
457
def setUp(self) -> None:
458
self.book = Book.objects.create(title='Test Book', author='Test Author')
459
460
def test_list_action(self) -> None:
461
"""Test list action via router URL."""
462
url = reverse('book-list')
463
response = self.client.get(url)
464
self.assertEqual(response.status_code, 200)
465
466
def test_detail_action(self) -> None:
467
"""Test retrieve action via router URL."""
468
url = reverse('book-detail', kwargs={'pk': self.book.pk})
469
response = self.client.get(url)
470
self.assertEqual(response.status_code, 200)
471
472
def test_custom_action(self) -> None:
473
"""Test custom action via router URL."""
474
url = reverse('book-popular')
475
response = self.client.get(url)
476
self.assertEqual(response.status_code, 200)
477
```
478
479
## Performance Considerations
480
481
### Route Optimization
482
483
```python { .api }
484
# Use SimpleRouter for better performance when root view not needed
485
router = SimpleRouter(trailing_slash=False) # Also saves URL matching
486
487
# Minimize registered viewsets
488
router.register(r'books', BookViewSet)
489
# Don't register unused viewsets
490
491
# Use basename when queryset is dynamic to avoid inspection
492
router.register(r'user-books', UserBookViewSet, basename='user-book')
493
```
494
495
### URL Caching
496
497
```python { .api }
498
from django.core.cache import cache
499
500
class CachedRouter(DefaultRouter):
501
"""Router with URL caching."""
502
503
def get_urls(self) -> list[URLPattern]:
504
cache_key = f"router_urls_{len(self.registry)}"
505
urls = cache.get(cache_key)
506
507
if urls is None:
508
urls = super().get_urls()
509
cache.set(cache_key, urls, timeout=3600) # Cache for 1 hour
510
511
return urls
512
```
513
514
This 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.