# toolshed/urls.py — the project's own top-level table
urlpatterns = [
path("admin/", admin.site.urls),
path("tools/", include("catalog.urls")),
path("loans/", include("loans.urls")),
]
# loans/urls.py — this app owns everything under /loans/
urlpatterns = [
path("<int:pk>/", views.loan_detail, name="loan-detail"),
path("<int:pk>/request/", views.request_loan, name="request-loan"),
path("mine/", views.MyActiveLoansView.as_view(), name="my-active-loans"),
]
include() is what lets loans own its own slice of the URL space, so the project's root table stays short and legible instead of listing every route in the app in one place. Path converters like <int:pk> extract and validate in the same breath — visit /loans/abc/request/ and it simply doesn't match that pattern at all, falling through to whatever's next (usually a 404), rather than handing the literal string "abc" to a view that expects an integer primary key.
<a href="{% url 'loan-detail' loan.pk %}">View this loan</a>
from django.urls import reverse
url = reverse("request-loan", args=[tool.pk])
Reference a URL by the name given to it in path(..., name=...) instead of writing /loans/12/ directly, and the actual path structure — /loans/ becoming /borrow-requests/, say — can change in exactly one place without touching a single template or redirect that points at it. A hardcoded URL string buried in a template is one of those things that works fine for months and then breaks silently the day someone reorganizes urls.py without realizing anything outside it was relying on the old shape.