3 Jul 2025
Flask, nanodjango and other frameworks have an @app.route()
decorator to handle routing. But standard Django practice is to have our urls specified away from the views they serve, in an ever increasing list of hard-to-scan urlpatterns.
But what if, just for a moment, we let URLs mingle with our views? What if, and bear with me here, we used decorators to define urlpatterns
right on top of the views they map to? Welcome to the wild world of view-centric routing.
By default, Django wants you to put all your URL patterns in urls.py
, and your views in views.py
, like a strict dormitory supervisor keeping the girls on one floor and the boys on another. This is usually fine—until you:
urls.py
and spend 30 minutes debugging a 404,If only URLs could stick closer to the thing they route to!
Let’s borrow a page from friendly neighbourhood micro-frameworks, and define a @route
decorator. (Flask developers, you may now smugly nod and sip your coffee.)
Here’s how you do it:
In routing.py
:
from django.urls import path _registry = [] def route(pattern, *args, **kwargs): def decorator(view): _registry.append((pattern, view, args, kwargs)) return view return decorator def get_urlpatterns(): return [path(pattern, view, *args, **kwargs) for (pattern, view, args, kwargs) in _registry]
In your views.py
:
from .routing import route @route('hello/<str:name>/', name='hello') def hello(request, name): from django.http import HttpResponse return HttpResponse(f'Why hello there, {name}!')
And finally, in
urls.py
:from .routing import get_urlpatterns urlpatterns = [ # ...maybe some old-fashioned ones... ] urlpatterns += get_urlpatterns()
@route
decorators. Try that in vanilla Django—without a decorator, despair is likely.urls.py
doesn’t import every module with a decorated view, those views won’t appear in your routing table. Don’t say I didn’t warn you.grep
.
Can I still use other decorators, like @login_required
?
Yes! Just make sure @route
is closest to the function definition. Like a good friend, it likes to be up front. (Technically, order matters for which version of your view gets routed.)
What about class-based views?
Slightly tricker, but the same principles can apply, with a bit more magic. Leave a comment if you want details.
Is it slower?
Nope. The decorator only runs at import time. After startup, Django’s routing is as zippy as ever.
Will this make me more attractive to other developers?
Anecdotally, yes.
Adopting decorator-based route registration in Django is like letting your URLs crash on your views’ sofa: sometimes messy, but with added convenience. It makes route discovery a breeze for small to moderate-sized projects and can even work for larger teams with good documentation (or a script that prints all registered routes).
Just remember: with great power comes great responsibility—and possibly, a need to explain your choices at the next code review.
Happy routing!
Have thoughts? Want a CBV-compatible version? Spot a problem? Reach out by email or by carrier pigeon.