Routing#
Our Controller is built without knowing anything about its future URL. Why so?
Because Django already has an amazing URL routing system and we don’t need to duplicate it
Because all controllers might be used in multiple URLs, for example in
/api/v1/and/api/v2/. Our design allows any possible customizations
1# Our `path` is an optimized drop-in replacement of `django.urls.path`:
2from dmr.routing import Router, path
3from examples.getting_started.pydantic_controller import UserController
4
5# Router is just a collection of regular Django urls:
6router = Router(
7 'api/',
8 [
9 path(
10 'user/',
11 UserController.as_view(),
12 name='users',
13 ),
14 ],
15)
16
17# Just a regular `urlpatterns` definition, Django-style:
18urlpatterns = [
19 router.to_urlpatterns(namespace='api'),
20]
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
{"email":"user@wms.org","uid":"1c761260-9f6b-42fa-acc2-d77ebc99ac75"}
Notice our helper method to_urlpatterns().
It can be used directly, or if some users prefer,
it can be replaced with a raw path() call:
urlpatterns = [
path(router.prefix, include((router.urls, 'my_app'), namespace='api')),
]
Which is semantically identical.
Note
If you want to parse path parameters, see Path parameters
and dmr.components.Path.
Including routers from other apps#
Added in version 0.13.0.
It is common in Django to use multiple apps with their own URLs and views.
They might define their own Router instances,
which can be included into the main router.
To do so, use the include() method:
1router = Router(prefix='api/')
2router.include(model_simple_urls.router, namespace='model_simple')
3router.include(model_fk_urls.router, namespace='model_fk')
4router.include(model_cursor_urls.router, namespace='model_cursor')
5router.include(middleware_urls.router, namespace='middlewares')
6router.include(controllers_urls.router, namespace='controllers')
7router.include(negotiations_urls.router, namespace='negotiations')
8router.include(jwt_auth_urls.router, namespace='jwt_auth')
9router.include(django_session_auth_urls.router, namespace='django_session_auth')
10router.include(token_auth_urls.router, namespace='token_auth')
11router.include(token_custom_user_urls.router, namespace='token_custom_user')
12router.include(etag_urls.router, namespace='etag')
13router.include(external_views_urls.router, namespace='external_views')
14router.include(allauth_auth_urls.router, namespace='allauth_auth')
15# ^ you can include as many routers as you wish!
It automatically uses our own faster path() function.
Basically, including another router is the same as writing:
router = Router(
prefix='api/',
urls=[
path(
other.router.prefix,
include(
(other_router.router.urls, 'app_name'),
namespace='namespace',
),
),
],
)
Use it to reduce boilerplate code.
You can also use to_urlpatterns() if you prefer.
It is semantically identical, except for the metadata. include correctly
handles nested metadata like tags, deprecated and ignore_from_spec,
while using to_urlpatterns ignores it.
So, if you don’t use the router-level metadata,
it does not matter which one you use.
Handling 404 errors#
By default, Django returns HTML 404 pages.
This is not what we want for API endpoints.
Instead, we want to return API responses with proper error structure and
content negotiation (e.g. JSON or XML based on the Accept header).
But, we still want HTML 404 pages for non-API views.
Important
Overriding django.conf.urls.handler404 has no effect
while DEBUG = True is set.
This is how Django behaves: https://docs.djangoproject.com/en/stable/ref/views/#the-404-page-not-found-view
To achieve this, you can use the
build_404_handler() helper.
It creates a handler that returns API-style 404 responses for specific path
prefixes (using the same serializer and renderer as your API), and falls back
to Django’s default handler for everything else.
Here is how you can use it in your root urls.py
(in your ROOT_URLCONF):
1import pydantic
2
3from dmr import Body, Controller
4from dmr.plugins.pydantic import PydanticFastSerializer
5from dmr.routing import Router, build_404_handler, path
6
7
8class UserCreateModel(pydantic.BaseModel):
9 email: str
10
11
12class UserController(Controller[PydanticFastSerializer]):
13 async def post(self, parsed_body: Body[UserCreateModel]) -> UserCreateModel:
14 return parsed_body
15
16
17router = Router(
18 'api/',
19 [
20 path('user/', UserController.as_view(), name='users'),
21 ],
22)
23
24urlpatterns = [
25 router.to_urlpatterns(namespace='api'),
26]
27
28handler404 = build_404_handler(router.prefix, serializer=PydanticFastSerializer)
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "correct@example.com"}' -H 'Content-Type: application/json'
{"email":"correct@example.com"}
$ curl http://127.0.0.1:8000/api/wrong/ -D - -X POST -d '{"email": "correct@old-domain.com"}' -H 'Content-Type: application/json'
HTTP/1.1 404 Not Found
date: Wed, 07 Oct 2026 07:03:56 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 56
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Page not found","type":"not_found"}]}
This returns JSON responses for api/ prefixed paths.
But, will still return regular Django HTML responses for any other path.
Handling 500 errors#
By default, Django returns HTML 500 pages.
This is not what we want for API endpoints.
Instead, we want to return API responses with proper error structure and
content negotiation (e.g. JSON or XML based on the Accept header).
But, we still want HTML 500 pages for non-API views.
Important
Overriding django.conf.urls.handler500 has no effect
while DEBUG = True is set.
This is how Django behaves: https://docs.djangoproject.com/en/stable/ref/views/#the-500-server-error-view
To achieve this, you can use the
build_500_handler() helper.
It creates a handler that returns API-style 500 responses for specific path
prefixes (using the same serializer and renderer as your API), and falls back
to Django’s default handler for everything else.
Here is how you can use it in your root urls.py
(in your ROOT_URLCONF):
1import pydantic
2
3from dmr import Body, Controller
4from dmr.plugins.pydantic import PydanticFastSerializer
5from dmr.routing import Router, build_500_handler, path
6
7
8class UserCreateModel(pydantic.BaseModel):
9 email: str
10
11
12class UserController(Controller[PydanticFastSerializer]):
13 async def post(self, parsed_body: Body[UserCreateModel]) -> UserCreateModel:
14 if parsed_body.email.endswith('@old-domain.com'):
15 raise RuntimeError('This error will be handled by handler500')
16 return parsed_body
17
18
19router = Router(
20 'api/',
21 [
22 path('user/', UserController.as_view(), name='users'),
23 ],
24)
25
26urlpatterns = [
27 router.to_urlpatterns(namespace='api'),
28]
29
30handler500 = build_500_handler(router.prefix, serializer=PydanticFastSerializer)
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "correct@example.com"}' -H 'Content-Type: application/json'
{"email":"correct@example.com"}
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "correct@old-domain.com"}' -H 'Content-Type: application/json'
HTTP/1.1 500 Internal Server Error
date: Wed, 07 Oct 2026 07:03:57 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 68
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Internal server error","type":"internal_error"}]}
See also
See Error handling if you want to learn how to handle
different errors based on different levels and fix these 500
exceptions.
Optimized URL Routing#
django-modern-rest provides
an optimized dmr.routing.path() function
that is a drop-in replacement for Django’s django.urls.path().
The custom implementation uses prefix-based pattern matching for faster routing. Instead of immediately running Django’s regex engine on every request, it performs a quick prefix check first.
Performance Impact#
Benchmark results on a MacBook Pro M4 Pro:
Best case: 9% faster (match found in the first few URL patterns)
Average case: 13% faster (match found in the middle of URL patterns list)
Worst case: 31% faster (404 Not Found, all patterns checked)
The prefix-based optimization dramatically reduces regex operations:
Static routes: Simple string comparison (no regex at all)
Dynamic routes: Regex only runs when the prefix matches
Failed matches: Eliminated in one operation (startswith check)
This is especially beneficial for applications with:
Large number of routes
High traffic
Migration#
Simply replace Django’s path with dmr.routing.path():
# Instead of ``from django.urls import path``:
from dmr.routing import path
from django.urls import include
urlpatterns = [
path('api/', include('myapp.urls')),
]
This is a drop-in replacement with no API changes required.
External views#
django-modern-rest can include URLs from other frameworks.
It can also reuse an existing OpenAPI schema
giving you the most freedom possible.
If you need this advanced feature, see the External views guide.