Choosing Between DefaultRouter and SimpleRouter for Clean URL Design in DRF
DefaultRouter adds a public API root and trailing‑slash redirects that can leak endpoints and add latency. SimpleRouter removes both while keeping the same ViewSet‑to‑route mapping. This post shows how to swap routers, verify the generated URLs, and decide which trade‑offs fit your production API.
05 Jul 2026, 19:41 UTC

The problem: unwanted redirects and public API roots
When you start a new Django REST Framework project, the tutorial usually registers a DefaultRouter and you get an API root at /api/ plus automatic trailing‑slash redirects. That works fine for a quick prototype, but in production it can cause two headaches:
- Clients that request
/api/users(no slash) receive a 301 redirect to/api/users/. Some HTTP libraries follow the redirect silently, others surface it as an error, and the extra round‑trip adds latency. - The browsable API root lists every registered endpoint. If you mount the router under a public path, you may unintentionally expose internal routes to anyone who visits
/api/.
Both issues stem from DefaultRouter’s defaults. SimpleRouter was added precisely to give developers stricter URL control without sacrificing the ViewSet‑to‑route mapping.
DefaultRouter vs SimpleRouter: what changes
| Feature | DefaultRouter | SimpleRouter |
|---|---|---|
| API root view | Yes (at the router prefix) | No |
| Trailing‑slash redirect | Yes (301 to /prefix/) | No – URLs are registered exactly as you write them |
| Generated route names | {basename}-list, {basename}-detail | Same naming scheme |
| Typical use case | Quick prototypes, browsable API exploration | Production APIs, strict client contracts, mounting under existing URLconf |
Both routers still map the standard ViewSet actions (list, create, retrieve, update, partial_update, destroy) to the same HTTP verbs. The difference is only in the extra root view and the slash handling.
Worked example: migrating a user API from DefaultRouter to SimpleRouter
Assume a project with a UserViewSet registered under api/. The current urls.py looks like this:
# urls.py (before)
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from myapp.views import UserViewSet
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = [
path('api/', include(router.urls)),
]
Step 1 – swap the router class:
# urls.py (after)
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register(r'users', UserViewSet, basename='user')
# no other changes required
Step 2 – verify the generated routes. Run the following command in the project root (requires django-extensions installed):
python manage.py show_urls --format=simple | grep users
Expected output (order may vary):
/api/users/ user-list GET,POST
/api/users/{pk}/ user-detail GET,PUT,PATCH,DELETE
Notice the trailing slash is now part of the pattern; there is no /api/ root entry.
Step 3 – test the trailing‑slash behavior with curl (run from any machine that can reach the dev server):
# With DefaultRouter you would see a 301
curl -I http://localhost:8000/api/users
# With SimpleRouter you get 404 (or 200 if you request /api/users/)
curl -I http://localhost:8000/api/users/
If your clients already send the trailing slash, nothing changes. If they don’t, you now have a clear 404 instead of a redirect, making the contract explicit.
Trade‑offs and limitations
- No automatic API root. If you rely on the browsable API root for discovery during development, you’ll need to add a custom view or keep a separate
DefaultRouterinstance mounted at a private path. - Nested routes still require manual work. Neither router builds
/users/{pk}/posts/automatically. You’ll either write a custompath()entry or adopt a third‑party package likedrf-nested-routers. - Bulk operations are not built‑in. Whether you use
DefaultRouterorSimpleRouter, creating/updating many objects in one request needs a custom@action(detail=False, methods=['post'])with a serializer that acceptsmany=Trueand atransaction.atomic()block.
These limitations are identical for both routers; the decision hinges on whether you want the extra root view and slash redirects.
Actionable checklist for your next API
- Decide on a URL contract: require trailing slashes (use
SimpleRouter) or accept both (keepDefaultRouterand document the redirect). - If you choose
SimpleRouter, add a health‑check or documentation endpoint manually if you need a discoverable entry point. - Run
python manage.py show_urlsafter every router change to confirm the generated patterns match your contract. - Write a small integration test that requests the non‑slash version and asserts a 404 (or 301 if you stay with
DefaultRouter). - Document the chosen style in your API style guide so future contributors don’t accidentally re‑introduce the other router.
Following this checklist keeps your URL surface predictable, avoids surprise redirects, and prevents accidental enumeration of internal endpoints.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.