(Django) View-Centric Routing: The Decorator Approach

If only URL declarations were closer to the view they route to.

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.

The Problem: URLs by Committee

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:

  • Forget to add a view to urls.py and spend 30 minutes debugging a 404,
  • Want to give a junior developer a tour of your codebase and need to look in three places just to answer, “what's the URL for this view?”
  • Realize you have three identical function names spread across three apps, each doing roughly the same thing but none of them listed where you thought.

If only URLs could stick closer to the thing they route to!

The Solution: A Route Decorator That Registers Itself

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()

The Benefits: Like Structuring, But More Fun

  • Single Source of Truth: Want to know all URLs for a view? Just look above it. No more cross-file scavenger hunts.
  • Copy/Paste Happiness: Need to move a view? Take its URL with it! Like luggage, but lighter.
  • Multiple Routes, No Sweat: You want two, three, or seventeen URLs for a view? Just stack those @route decorators. Try that in vanilla Django—without a decorator, despair is likely.
  • Beginner-Friendly: New devs can actually find routes, reducing onboarding times and blood pressure.
  • Inspired by Flask, Works in Django: Combine the URL convenience of Flask with the horsepower of Django.

The (Minor) Drawbacks: Because There’s Always a Catch

  • Startup Dependency on Import Order: If your 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.
  • Loss of Bird’s-Eye View: Want to see all your URLs in one spot? You’ll need to add some extra code, or develop a fondness for grep.
  • Slight Memory Overhead: Your registry keeps references to each decorated view. Honestly, if this tips your server over, you’re probably already being considered as a case study in “Just How Many Views is Too Many.”
  • Tradition Is Powerful: Not everyone likes change. If you push this to production, expect at least three emails from the oldest member of your team, two of which will contain the phrase "not how Django does it."

FAQ (Frequently Anticipated Quibbles)

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.

Conclusion

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.