ADR-032: Url Names Derived From View Classes
Status
Accepted
Context
Django names each url pattern with a string, path(..., name="..."), and code finds the url by that
string, with reverse() or the {% url %} template tag. Nothing ties the string to the view that
serves the url, so names drift: a view is renamed and its url name isn’t, two modules pick the same
convention differently, and a typo in a caller fails only when that line runs.
Smarter has about 60 urls.py modules, and most of them already solved this the same way: a class
beside the url patterns whose attributes are the names, each computed from the view class with
to_snake_case(View.__name__), and callers that reverse through the class. A survey of the code
found the rest in several older shapes:
literal names in upper-case constants (
ACCOUNT_LOGIN = "account_login") or lower-case ones (chathistory = "chathistory"), andname="..."literals with no class at all, mostly inapi/v1modules;a namespace with a trailing colon (
"api:v1:cli:"), used asnamespace + name, beside namespaces without one, used asf"{namespace}:{name}";_by_idsuffixes written with and without their underscore, which gives names such asllm_client_viewby_id;{% url 'namespace:name' %}tags in templates, which hard-code the derived string.
Decision
Every url name is derived from the view class that serves it, in one place, and every caller reads it from there.
The namespace is a constant,
namespacein the module’sconst.py. The urls module setsapp_name = namespace, and its parent includes it with the same constant.Each urls module has one class of names,
<App>ReverseNames(<App>ApiV1ReverseViewsfor an api version). Its first attribute isnamespace, without a trailing colon.Each name is
to_snake_case(<View>.__name__), or the function’s__name__for a function view. The attribute’s name is the route’s role (listview,deployment_by_hashed_id), so that callers don’t change when a view is renamed.A view with several routes adds a suffix to its derived name:
_all,_by_id,_by_hashed_id, each starting with an underscore.Every
path()names its route with a class attribute, and every caller reverses through the class, withsmarter.lib.django.shortcuts.reverse(): the namespaces and the name as separate arguments, or, with url kwargs, one string joined with an f-string.Templates receive urls, not names. A view or context processor reverses the url and passes it in the context.
Code that can run on a deployed LLMClient’s host reverses with
urlconf=settings.ROOT_URLCONF, because that host’s urlconf has none of the console’s names, and a url that leaves the console is made absolute on the platform’s domain.
The newest modules are the reference: smarter.apps.guardrail.urls, smarter.apps.llmclient.urls,
smarter.apps.prompt.urls and smarter.apps.guardrail.api.v1.urls. The smarter-url-names
Agent Skill, in skills/, gives the recipe, and lists the older modules that don’t follow it yet.
Alternatives Considered
Literal names, by convention. Django’s default. It works while one person writes the code, and drifts as soon as several do, which is what the survey found.
Names derived from the url path. Paths change for users’ sake (a new prefix, a renamed segment), and a path with parameters makes an awkward name.
No names: hard-coded paths. Every route move would be a search through the codebase and the templates, with no failure when one is missed.
Consequences
Positive:
A name can’t drift from its view, and a typo in a caller fails at import, not at runtime.
Names are predictable: anyone who knows the view knows its name, and nobody has to choose one.
One grep for the class attribute finds every caller of a route.
Negative:
Renaming a view renames its url name. Callers that go through the class follow, but a literal copy of the old string, in a template or a test, breaks, so a rename must grep for the string too.
to_snake_casesplits acronyms into words (LLMClient→llm_client), so the derived names are long, and occasionally surprising when read in a log.A urls module must import its views, so a module whose views import it back needs its names class in a separate
names.py.The older modules remain until someone changes them for another reason.