In our previous blog, how the Web Talks to Django, we saw how Django handles requests and responses internally. Now that you have a clear picture of what happens when a user types a web address and hits enter, it’s time to take the next step: building that bridge between the browser and your Django code.
In this blog, we’ll focus on Django’s URL system, the backbone of how requests find their way to the right view(" a chunk of Python code"). We will explore more about **views **later.
You’ll learn how to set up a Django project, declare URLs, organize them into logical groups, and even extract useful information directly from the URL to use in your views.
Getting Django Set Up
We’ll walk step by step through setting up your first Django project and running it locally.
Django runs on Python, so the first step is to make sure you have it installed. Run:
python --version
If not installed, visit python and follow instructions to install Python.
Then, we need a place to put our work. You can name your project the name you prefer. I am going to use "mastering-django".
mkdir mastering-django
cd mastering-django
Next, we install Django into a virtual environment so we keep our project dependencies separate from the rest of the installed Python packages on our machine.
python3 -m venv venv
Activate it:
source venv/bin/activate
With your environment active, install Django:
(venv) $ pip install Django
Django comes with some tools that we can use to get a project started quickly. let’s start a new Django project:
(venv) $ django-admin startproject myproject .
When you run django-admin startproject myproject ., Django creates a project structure for you. It might look like a lot at first, but each file has a clear purpose. Let’s walk through them.
manage.py
myproject/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py
manage.py
This is your project’s remote control. It’s a command-line tool that lets you:
- Start the development server
- Apply database migrations
- Create apps
- Manage users (like creating a superuser)
Whenever you interact with your project, you’ll usually do it through manage.py.
The Inner myproject/ Folder
This folder holds your project’s configuration files.
init.py
Marks the folder as a Python package. It’s usually empty, but it tells Python “this is code you can import.”settings.py
The brain of your project. It stores all configurations — installed apps, database setup, middleware, templates, static files, and more.urls.py
The map of your project. It connects browser requests (like /about/ or /blog/) to the right views in your code.asgi.py
Entry point for ASGI servers. It allows Django to support modern features like WebSockets and async handling.wsgi.py
Entry point for WSGI servers. This is what most traditional web servers (like Gunicorn or uWSGI) use to run Django apps in production.
By understanding these files, you know where your configurations live and how Django connects the dots.
Next, let’s run the development server to make sure everything is working correctly:
(venv) $ python manage.py runserver
If you open http://127.0.0.1:8000/ in your browser, you’ll see Django’s welcome page 🎉. That means your project is alive and ready
Now that our project is running and we understand its structure. Now it’s time to focus on one of the most important pieces: urls.py.
URL configuration in Django
Every time you type a URL into your browser and hit Enter, Django needs to know: where should this request go?
That’s where urls.py configuration comes in. Think of Django’s URL configuration (urls.py) as a list of paths. Whenever a request comes in, Django scans that list from top to bottom. As soon as it finds a match, it stops searching and routes the request to the matching view (note: a view is just a chunk of Python code that returns a response; we’ll dive into views in the next blog).
Here’s a simple example of a url.py:
# myproject/urls.py
from django.urls import path
from application import views
urlpatterns = [
path("", views.home),
path("about/", views.about),
path("contact/", views.contact),
path("terms/", views.terms),
]
If you visit https://www.example.com/about/, Django trims off the domain and leading slash, leaving just about/. That matches the second entry above, so the request is sent to views.about
.
And if you simply visit the root URL https://www.example.com/, Django matches the empty string " " and sends the request to views.home
.
The order matters. If you accidentally define two paths that could match the same URL, Django will always go with the first one it finds and ignore the rest.
Notice the trailing slash (/). By default, Django expects URLs to end with one. If you type https://www.example.com/about (without the slash), Django will redirect you to the correct version because of the APPEND_SLASH default setting.
Dynamic Paths with Converters
Static paths like /about/ are nice, but most real-world apps need to be flexible. Imagine if every blog post had to live at /blog/ with no way of telling them apart, that wouldn’t work.
Instead, we usually want URLs to carry information. For example:
- A blog post URL might include a year and a slug, like /blog/2024/django-rocks/.
- A user profile URL should work for any username, like /users/stephen/.
That’s where path converters come in. They let you pull pieces of information straight out of the URL and hand them directly to your view.
# myproject/urls.py
from django.urls import path
from application import views
urlpatterns = [
path("blog/<int:year>/", views.blog_by_year),
path("users/<str:username>/", views.profile),
]
Here’s what happens behind the scenes:
- If someone visits /blog/2024/, Django sees int:year and automatically captures 2024 as an integer. That value is then passed into views.blog_by_year(year=2024).
- If someone visits /users/stephen/, Django captures "stephen" as a string and hands it to your view: views.profile(username="stephen").
The best part? Django validates the values before they ever hit your code. If a user tries /blog/notayear/, Django knows that “notayear” is not an integer, so it returns a clean 404 Not Found without bothering your view.
In other words:
- You get dynamic, flexible URLs.
- You don’t need to write messy validation logic in your views.
- Your app stays both user-friendly and safe.
Think of converters as Django’s built-in parsers for your URLs. They keep your code clean while giving you the power to make URLs that feel natural and meaningful.
Regular Expression Paths
Sometimes, path converters aren’t enough. Let’s say you only want to match a four-digit year, such as 2025
. In that case, you can reach for re_path and use a regular expression (regex).
from django.urls import re_path
from application import views
urlpatterns = [
re_path(r"^blog/(?P<year>[0-9]{4})/(?P<slug>[\w-]+)/$", views.blog_post),
]
At first glance, regex looks intimidating, but here’s what’s happening:
- (?P[0-9]{4}) - captures exactly four digits and passes them as a variable called year.
- (?P[\w-]+) - captures a word-like slug (letters, numbers, underscores, or dashes).
So if someone visits /blog/2025/django-rocks/, Django will match it and call:
views.blog_post(year=2025, slug="django-rocks")
Regex is powerful, like a chainsaw. You can cut through particular patterns, but you don’t always need it. Most of the time, Django’s simple path converters (int, str, slug, etc.) are more than enough.
Grouping Related URLs
As your project grows, dumping all routes into one urls.py file can get messy. Instead, Django encourages you to let each app manage its own URLs and then connect them in the main project.
For example, suppose you have two apps: schools and students.
myproject/urls.py
from django.urls import path, include
urlpatterns = [
path("schools/", include("schools.urls")),
path("students/", include("students.urls")),
]
school.url
from django.urls import path
from . import views
urlpatterns = [
path("", views.index),
path("<int:school_id>/", views.school_detail),
]
students.urls
from django.urls import path
from . import views
urlpatterns = [
path("", views.index),
path("<int:student_id>/", views.student_detail),
]
Now, each app is responsible for its own routing, and the main project needs to know where to plug things in. This makes your project cleaner, modular, and much easier to maintain.
Naming URLs
Hardcoding URLs everywhere in your project is risky. Imagine you define a route at /blog/categories/, but later decide to move it to /marketing/blog/categories/. If you’ve hardcoded links, you’d need to hunt down and update every single one.
Instead, Django lets you name your URLs.
# project/urls.py
from django.urls import path
from blog import views
urlpatterns = [
path("marketing/blog/categories/", views.categories, name="blog_categories"),
]
Now you can reference this route by name instead of by path.
Example in a view:
from django.http import HttpResponseRedirect
from django.urls import reverse
def old_blog_categories(request):
return HttpResponseRedirect(reverse("blog_categories"))
Even if the path changes later, as long as the name (blog_categories) stays the same, your code keeps working.
For larger projects, you can even use namespaces (like schools:index vs. students:index) to avoid naming conflicts.
Wrapping Up
And that’s a full tour of Django’s URL system! 🎉
Here’s what we covered:
- How Django matches requests against urlpatterns.
- Why the order of your routes matters.
- How to use path converters to capture values from the URL.
- When to reach for re_path and regex for advanced matching.
- How to group URLs by app using include().
- How naming (and namespacing) URLs makes your project more flexible.
URLs are the map of your Django project. They decide how a user’s request finds its way to the right piece of code. Once you understand this mapping system, everything else in Django starts to click into place.
In the next blog, we’ll zoom in on the views, those chunks of Python code that actually generate the response. That’s where we’ll really bring our URLs to life. 🚀
The Big Picture
Here’s the request flow at a glance:
- Browser – the user types in a URL and hits enter.
- Django URLconf (urls.py) – Django looks through your list of paths to find a match.
- View – the matching view (Python function or class) runs and decides what to return.
- Response – Django sends the result (HTML, JSON, redirect, etc.) back to the browser.
Top comments (0)