Nested urls.py challenge

I would like to achieve the following routing:

example.com/recipes → [List of recipe categories]
example.com/recipes/soups → [List recipes for a certain category, in this case, soups]
example.com/recipe/lentil-soup → [Recipe, note the singular ‘recipe’ in the URL]

This is not very complicated when using the project’s root urls.py, but I struggle to get it right with nested url configuration. This is what I tried:

# mysite/mysite/urls.py
urlpatterns = [
    re_path(r'^recipes?/', include('recipes.urls')),
]

# mysite/recipes/urls.py
urlpatterns = [
    re_path(r'^/?$', views.categories, name="categories"),
    re_path(r'^(?P<url_name>[\-_a-z0-9]+)/?$', views.category, name='category'),
    re_path(r'^(?P<url_name>[\-_a-z0-9]+)/?$', views.recipe, name='recipe'),
]

There are two (obvious) problems: As “^recipes?/$” is matched at the root level, the first pattern will match example.com/recipes/, but not example.com/recipes, and if I change the root level pattern to "^recipes?/?$, the more specific patterns will create incorrect matches in reverse URLs (such as example.com/recipessoups). What’s more, the second and third patterns are indistinguishable because I cannot access the difference between plural and singular in the nested urls.py.

Should I just give up and add three patterns to my root urls.py, or is there a way to achieve my goal with nested url configuration?

I’m not sure from your example what is a constant that defines a grouping and what are variables.

What I’m interpreting:
recipes → Root level url to show the list of categories
recipes/<str:category> → Shows list of recipes for “category”
recipe/<str:recipe> → Shows the specific recipe.

Is this interpretation correct? If so, there is no clean nested structure for all these in one definition. I’d suggest either rethinking your url structure and naming conventions or simply including these three entries in your root urls.

Additionally, the file urls.py is just a naming convention, you can name it whetever you want it. With this in mind, you can have different url files for different things. For example:

# mysite/mysite/urls.py
urlpatterns = [
    re_path(r'^recipes?/', include('recipes.urls')),
    re_path(r'^recipe?/', include('recipes.single_recipe_urls')), 
]

# mysite/recipes/urls.py
urlpatterns = [
    re_path(r'^/?$', views.categories, name="categories"),
    re_path(r'^(?P<url_name>[\-_a-z0-9]+)/?$', views.category, name='category'),
    re_path(r'^(?P<url_name>[\-_a-z0-9]+)/?$', views.recipe, name='recipe'),
]

# mysyte/recipes/single_recipes_urls.py
urlpatterns = [
    re_path(r'^(?P<url_name>[\-_a-z0-9]+)/?$', views.recipe, name='recipe'),
]

But maybe you’re over complicating. You don’t necessarily need to have a prefix for a included urls file. Using your current example, with a single urls.py file. You can:

# mysite/mysite/urls.py
urlpatterns = [
    path('', include('recipes.urls')),  # < -- Remove the prefix from here
]

# mysite/recipes/urls.py

# Add the each desired prefix on each entry, more repetitive.
urlpatterns = [
    re_path(r'^recipes/?$', views.categories, name="categories"),
    re_path(r'^recipes/(?P<url_name>[\-_a-z0-9]+)/?$', views.category, name='category'),
    re_path(r'^recipe/(?P<url_name>[\-_a-z0-9]+)/?$', views.recipe, name='recipe'),
]

Yes, this is correct. Leandro provided a nice solution which does not literally do what I was looking for, but keeps the URLs for the recipes app in a separate configuration file (and this is what actually matters).

Thank you! I never thought of leaving out the prefix.

I would also highly recommend switching to path() over re_path.

If you’re recommending something, it would be good to provide the “why”, otherwise this can sound like just personal preference.
Both ways are supported by Django, and Django does not encourages or discourages any way of doing it, both path and re_path are valid ways to defining a url pattern.

1 Like

Actually, one could interpret the docs to say that they do express a preference toward using path instead of re_path.

Quoting directly from the first paragraph at Using regular expressions

If the paths and converters syntax isn’t sufficient for defining your URL patterns, you can also use regular expressions.

This can be read as implying that path is the preferred method, and re_path is used when you can’t use path.

(Agreed, that interpretation could be considered a bit of a stretch.)

Beyond that, I think the key advantage of path is that it provides the facility for creating custom path validation more easily than what would be possible with a pure regex.

For example, let’s say that you have a url that accepts an IPv4 address as a url segment. A full regex to validate that address in all its possible variations is a bit extensive, depending upon exactly what variations you wish to accept as input.
However, with path, you can register a custom converter and use the Python ipaddress module to validate it.

This allows certain types of constrained url values to be validated before being passed to a view for processing.

2 Likes

I think if you read the docs for path() you would see very quickly why. But let’s look at two examples from your code:

    re_path(r'^recipes/?$', views.categories, name="categories"),
    re_path(r'^recipes/(?P<url_name>[\-_a-z0-9]+)/?$', views.category, name='category'),

which is shorter, easier to read and a lot easier to write as:

   path('recipes/', views.categories, name="categories"),
   path('recipes/<str:url_name>/', views.category, name='category'),

assuming you turn on the append slash feature of course, which I also highly recommend. The why in that case is that you want your urls to be predictable, and relative URLs to work as you’d expect. For example from /foo/bar/ the path .. would mean /foo/, but from /foo/bar it would mean /. This is super confusing, and a potential source of bugs.

I agree with Anders that path is clearer and simpler, but I dislike the append slash approach (using a redirect), and prefer a regex pattern with an optional trailing slash. Since Django provides the APPEND_SLASH setting, I assume there is no equivalent for /?$ with the path directive.

Allowing both /my/path and /my/path/ can be pretty confusing, because some things, like search engines, may treat them as separate URLs containing the same content. They might not… but they might. Better to remove ambiguity and redirect from one option to your preferred other option.

1 Like

I’ve heard people say they dislike it, but never any technical reasons for that. The technical reasons for appending slash always are pretty strong, so I think there should be a strong technical reason against. Do you have any that you can share? I generally believe in “there are only tradeoffs” but in this case the reasons on one side are very strong, and I don’t see any reasons for the other side at all really (although I suppose 404 on no-slash is a reasonable position, I’d accept that).

I decided to go with the 404 for slashed URLs – it is my own website, after all. :smiley: