In this chapter, we will build a Django project that says “Hello, World” on the homepage, the traditional way to start a new web framework. We’ll also work with Git for the first time and deploy our code to GitHub. The complete source code for this and all future chapters is available online at the official GitHub repo for the book.

How Websites Work

If you are new to programming and building websites, it is worth quickly reviewing the fundamentals behind the Internet and the World Wide Web. The Internet is a broad global system of interconnected computers; the World Wide Web is a subset of the Internet that refers to hypertext documents linked together via hyperlinks (in other words, webpages).

The internet relies on various “communication protocols,” which are like human languages in that they allow computers all over the world to communicate with one another via agreed-upon conventions. For example, file sharing uses the File Transfer Protocol (FTP), sending email uses the Simple Mail Transfer Protocol (SMTP), communicating through voice uses the Voice over Internet Protocol (VoIP), and viewing webpages–our area of particular interest in this book–uses the Hypertext Transfer Protocol (HTTP).

In common conversation, the terms “Internet” and “World Wide Web” are frequently used interchangeably, but as web developers it is important to realize that they refer to quite different things.

Underpinning the world wide web is the client-server model. A “client” refers to any internet-connected device making service requests, such as a computer, a phone, a dishwasher, etc.; the “server” is computer hardware or software that responds to service requests. In other words, the client makes a request and the server returns a response.

The computers powering the internet are often referred to as servers, but really they’re just computers connected to the internet all the time running special software that lets them “serve” information to other computers. Your own computer can be a server, but in practice most servers exist in large data centers (aka “the cloud”).

Since we are using the HTTP protocol for all of this, we can be more specific and say that a client makes an HTTP request and a server responds with an HTTP response.

HTTP request/response cycle

The full domain name for a website like LearnDjango.com is actually https://learndjango.com. The https:// at the beginning specifies that we are using HTTP as our protocol: HTTPS is the encrypted version of HTTP and now accounts for the majority of web traffic. Modern web browsers will automatically add this on for you so most regular users simply type the domain name and are unaware of the HTTP underpinnings.

Each time you type a URL address into your web browser–for example https://learndjango.com–an HTTP request is sent to the appropriate server which then returns an HTTP response. Your web browser then renders the data from the HTTP response to create a webpage. Every time you click on a link or request a new URL this HTTP request/response cycle begins again. Back and forth the communication goes.

In production, a Django website like LearnDjango.com is hosted on a physical server and automatically processes HTTP requests and responses. It relies on additional machinery that we will build out during later projects in the book. In local development, things are much simpler. Django comes with a lightweight development server, runserver, that manages HTTP requests and responses, helps Django generate dynamic content from the database and serves static files (more on these later). It’s quite powerful. We can therefore update our first image with a new one featuring runserver wrapped around Django.

Django HTTP request/response cycle

If you want to see the actual raw data included in an HTTP response, find the “View Source” option in your web browser of choice. In the Chrome web browser, at the top of the window go to View -> Developer -> View Source to take a look. It isn’t very human-readable! That’s why we use web browsers to compile the responses into human-readable webpages.

How Web Frameworks Work

There are two broad categories of websites: static and dynamic. A static website consists of individual HTML documents that are sent as-is over HTTP to your web browser. If your website has ten pages then there must be ten corresponding HTML files. This approach can work for very small websites but quickly falls apart when a website needs hundreds or thousands of pages. A dynamic website consists of a database, HTML templates, and an application server that can update the files before sending them to your browser via HTTP. Most large websites adopt this approach since it means millions of webpages can be composed of only a few HTML templates, a small amount of logic, and a big database.

Django is designed for dynamic websites and abstracts away much of the difficulty inherent in creating a website from scratch. If you think about it, most websites require the same fundamental tools:

  • a way to process URL requests
  • a way to connect to a database
  • a way to generate dynamic content by filtering data from the database
  • a way to create templates for styling HTML and adding CSS, images, etc as needed

Model-View-Controller vs Model-View-Template

If you have built websites before you might be familiar with the Model-View-Controller (MVC) pattern. It is used by web frameworks including Ruby on Rails, Spring (Java), Laravel (PHP), and ASP.NET (C#). This is a popular way to internally separate the data, logic, and display of an application into separate components that are easier for a developer to reason about.

In the traditional MVC pattern there are three major components:

  • Model: Manages data and core business logic
  • View: Renders data from the model in a particular format
  • Controller: Accepts user input and performs application-specific logic

Django’s approach is sometimes called Model-View-Template (MVT) but it is really a 4-part pattern that also incorporates URL configuration. Something like Model-View-Template-URL (MVTU) would be a more accurate description:

  • Model: Manages data and core business logic
  • View: Describes which data is sent to the user but not its presentation
  • Template: Presents the data as HTML with optional CSS, JavaScript, and static assets
  • URL Configuration: Regular expression components configured to a View

The “View” in MVC is analogous to a “Template” in Django while the “Controller” in MVC is divided into a Django “View” and “URL config.” This is understandably quite confusing to newcomers. To help, let’s map out the order of a given HTTP request/response cycle for Django.

When you type in a URL, such as https://djangoproject.com, the first thing that happens within our Django project is that runserver kicks into gear and helps Django look for a matching URL pattern (contained in urls.py). The URL pattern is linked to a single view (contained in views.py) which combines the data from the model (stored in models.py) and the styling from a template (any file ending in .html). The view then returns a HTTP response to the user.

A simplified version of this complete Django flow looks like this:

Django request/response cycle

If you are new to web development the distinction between MVC and MVT will not matter much. This book demonstrates the Django way of doing things so there won’t be confusion. However if you are a web developer with previous MVC experience, it can take a little while to shift your thinking to the “Django way” of doing things which is more loosely coupled and allows for easier modifications than the MVC approach.

Initial Set Up

To begin our first Django website, open up a new command line shell or use the built-in terminal on VS Code. For the latter, click on “Terminal” at the top and then “New Terminal” to bring it up on the bottom of the screen.

Make sure you are not in an existing virtual environment by checking there is nothing in parentheses before your command line prompt. You can even type deactivate to be completely sure. Then navigate to the code directory on your desktop and create a helloworld directory with the following commands.

# Windows
$ cd onedrive\desktop\code
$ mkdir helloworld
$ cd helloworld

# macOS
$ cd ~/desktop/code
$ mkdir helloworld
$ cd helloworld

Create a new virtual environment called .venv, activate it, and install Django with Pip as we did in the previous chapter. We can also install Black now, too.

# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install django~=4.2.0
(.venv) $ python -m pip install black

# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install django~=4.2.0
(.venv) $ python3 -m pip install black

Now we’ll use the Django startproject command to make a new project called django_project. Don’t forget to include the period (.) at the end of the command so that it is installed in our current directory.

(.venv) $ django-admin startproject django_project .

Let’s pause for a moment to examine the default project structure Django has provided for us. You can examine this visually by opening the new directory with your mouse on the desktop. The .venv directory may not be initially visible because it is a “hidden file” but it is nonetheless still there and contains information about our virtual environment.

├── django_project
│   ├── __init__.py
|   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── .venv/

Django has created a django_project directory and a manage.py file. Within django_project are five new files:

  • __init__.py indicates that the files in the folder are part of a Python package. Without this file, we cannot import files from another directory which we will be doing a lot of in Django!
  • asgi.py allows for an optional Asynchronous Server Gateway Interface to be run.
  • settings.py controls our Django project’s overall settings
  • urls.py tells Django which pages to build in response to a browser or URL request
  • wsgi.py stands for Web Server Gateway Interface, more on this in the next chapter when we do our first deployment

The manage.py file is not part of django_project but is used to execute various Django commands such as running the local web server or creating a new app.

Let’s try out our new project by using Django’s lightweight built-in web server for local development purposes.

(.venv) $ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly 
  apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
April 13, 2023 - 13:18:12
Django version 4.2, using settings 'django_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

If you visit http://127.0.0.1:8000/ you should see the following image:

Django welcome page

It is safe to ignore the warning about 18 unapplied migrations at this point. Django is complaining that we have not yet “migrated” our initial database. Since we won’t actually use a database in this chapter the warning won’t affect the end result. However, since warnings are annoying to see, we can remove it by first stopping the local server with the command Control+c and then running python manage.py migrate.

Note: Going forward when there is a common command for both Windows and macOS, python will be used as the default rather than referencing both python on Windows and python3 on macOS.

$ python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

What Django has done here is create a SQLite database and migrated its built-in apps provided for us. This is represented by the new db.sqlite3 file in our directory.

├── django_project
│   ├── __init__.py
|   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3  # new
├── manage.py
└── .venv/

If you execute python manage.py runserver again you should no longer see any warnings.

Create An App

A single Django project can contain many “apps,” which is an organizational technique for keeping our code clean and readable. Each app should control an isolated piece of functionality. For example, an e-commerce site might have one app for user authentication, another app for payments, and a third app to power item listing details. That’s three distinct apps that all live within one top-level project. Another example is if you build a social networking site–let’s call it a Twitter clone–that has an option to email users when someone comments on a post. Initially you might create a tweet app and build the email functionality within it. However over time the complexity of both the tweets and the emails being sent will likely grow and so splitting it into two apps–tweet and emails–could make sense. This is especially true if you want to reuse the email parts elsewhere in your larger project.

How and when you split functionality into apps is very subjective but a good rule of thumb is that when a single app feels like it’s doing too much, it is time to split features into their own apps which each have a single function.

To add a new app go to the command line and quit the running server with Control+c. Then use the startapp command followed by the name of our app which will be pages.

(.venv) $ python manage.py startapp pages

If you look visually at the helloworld directory Django has created within it a new pages directory containing the following files:

├── pages
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py

Let’s review what each new pages app file does:

  • admin.py is a configuration file for the built-in Django Admin app
  • apps.py is a configuration file for the app itself
  • migrations/ keeps track of any changes to our models.py file so it stays in sync with our database
  • models.py is where we define our database models which Django automatically translates into database tables
  • tests.py is for app-specific tests
  • views.py is where we handle the request/response logic for our web app

Notice that the model, view, and url from the MVT pattern are present from the beginning. The only thing missing is a template which we’ll add shortly.

Even though our new app exists within the Django project, Django doesn’t “know” about it until we explicitly add it to the django_project/settings.py file. In your text editor open the file up and scroll down to INSTALLED_APPS where you’ll see six built-in Django apps already there. Add pages at the bottom.

# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "pages",  # new
]

Your First View

For our first website we’ll create a Web page that outputs the text “Hello, World!” This is a static page that does not involve a database or even a templates file. Instead, it is a good introduction to how views and URLs work within Django.

A view is a Python function that accepts a Web request and returns a Web response. The response can be the HTML contents of a Web page, a redirect, a 404 error, an image, or really anything.

When a Web page is requested, Django automatically creates an HttpRequest object that contains metadata about the request. Then Django loads the appropriate view, passing the HttpRequest in as the first argument to the view function. The view is ultimately responsible for returning an HttpResponse object.

In our pages app there is already a file called views.py which comes with the following default text:

# pages/views.py
from django.shortcuts import render

# Create your views here.

render() is a Django shortcut function that can be used to create views, however there is an even simpler approach possible which is to instead use the built-in HttpResponse method. That’s what we’ll do here.

Update the pages/views.py file with the following code:

# pages/views.py
from django.http import HttpResponse


def home_page_view(request):
    return HttpResponse("Hello, World!")

There are two types of views in Django: function-based views (FBVs) and class-based views (CBVs). Our code in this example is a function-based view: it is relatively simple to implement and explicit. Django originally started with only FBVs but over time added CBVs which allow for much greater code reusability, keeps things DRY (Don’t-Repeat-Yourself), and can be extended via mixins. The additional abstraction of CBVs makes them quite powerful and concise, however it also makes them harder to read for Django newcomers.

Because web development quickly becomes repetitive, Django also comes with a number of built-in generic class-based views (GCBVs) to handle common use cases such as creating a new object, forms, list views, pagination, and so on. We will be using GCBVs heavily in this book in later chapters.

There are therefore, technically, three ways to write a view in Django: function-based views (FBVs), class-based views (CBVs), and generic class-based views (GCBVs). This customization is helpful for advanced developers but confusing for newcomers. Many Django developers–including your author–prefer to use GCBVs when possible and revert to CBVs or FBVs when required. By the end of this book you will have used all three and can make up your own mind on which approach you prefer.

Moving along we need to configure our URLs. In your text editor, create a new file called urls.py within the pages app. Then update it with the following code:

# pages/urls.py
from django.urls import path

from .views import home_page_view

urlpatterns = [
    path("", home_page_view, name="home"),
]

On the top line we import path from Django to power our URL pattern and on the next line we import our views. By referring to the views.py file as .views we are telling Django to look within the current directory for a views.py file and import the view home_page_view from there.

Our URL pattern here has three parts:

  • the empty string, ""
  • a reference to the view called home_page_view
  • an optional named URL pattern called "home"

In other words, if the user requests the homepage represented by the empty string "", Django should use the view called home_page_view.

We’re almost done at this point. The last step is to update our django_project/urls.py file. It’s common to have multiple apps within a single Django project, like pages here, and they each need their own dedicated URL path.

# django_project/urls.py
from django.contrib import admin
from django.urls import path, include  # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("pages.urls")),  # new
]

We’ve imported include on the second line next to path and then created a new URL pattern for our pages app. Now whenever a user visits the homepage, they will first be routed to the pages app and then to the homePageView view set in the pages/urls.py file.

This need for two separate urls.py files is often confusing to beginners. Think of the top-level django_project/urls.py as the gateway to various URL patterns distinct to each app.

We have all the code we need now. To confirm everything works as expected, restart our Django server:

(.venv) $ python manage.py runserver

If you refresh the browser for http://127.0.0.1:8000/ it now displays the text “Hello, World!”

Hello World homepage

Git

In the previous chapter, we installed the version control system Git. Let’s use it here. The first step is to initialize (or add) Git to our repository. Make sure you’ve stopped the local server with Control+c, then run the command git init.

(.venv) $ git init

If you then type git status you’ll see a list of changes since the last Git commit. Since this is our first commit, this list is all of our changes so far.

(.venv) $ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        .venv
        django_project/
        db.sqlite3
        manage.py
        pages/

nothing added to commit but untracked files present (use "git add" to track)

Note that our virtual environment .venv is included which is not a best practice. It should be kept out of Git source control since secret information such as API keys and the like are often included in it. The solution is to create a new file in the project-level directory called .gitignore which tells Git what to ignore. The period at the beginning indicates this is a “hidden” file. The file still exists but it is a way to communicate to developers that the contents are probably meant for configuration and not source control.

Here is how your project structure should look now:

├── django_project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── pages
│   ├── migrations
|      ├── __init__.py
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── .gitignore  # new
├── db.sqlite3
├── manage.py
└── .venv/

In this new .gitignore file, add a single line for .venv.

.venv/

If you run git status again you will see that .venv is not longer there. It has been “ignored” by Git.

At the same time, we do want a record of packages installed in our virtual environment. The current best practice is to create a requirements.txt file with this information. The command pip freeze will output the contents of your current virtual environment and by using the > operator we can do all this in one step: output the contents into a new file called requirements.txt. If your server is still running enter Ctrl+c and Enter to exit before entering this command.

(.venv) $ pip freeze > requirements.txt

A new requirements.txt file will appear with all our installed packages and their dependencies. If you look inside this file you’ll see there are actually nine packages even though we have installed only two: Django and Black. That’s because Django and Black depend on other packages, too. It is often the case that when you install one Python package you’re also installing multiple dependent packages, too. Since it is difficult to keep track of all the packages a requirements.txt file is very important.

asgiref==3.6.0
black==23.3.0
click==8.1.3
Django==4.2
mypy-extensions==1.0.0
packaging==23.1
pathspec==0.11.1
platformdirs==3.2.0
sqlparse==0.4.3

Next want to perform our first Git commit to store all the recent changes. Git comes with a lengthy list of options/flags that can be used. For example, to add all recent changes we can use git add -A. And then to commit the changes we will use a -m flag (this one stands for “message”) to describe what has changed. It is very important to always add a message to your commits since most projects will easily have hundreds if not thousands of commits. Adding a descriptive message each time helps with debugging efforts later on since you can search through your commit history.

(.venv) $ git add -A
(.venv) $ git commit -m "initial commit"

In professional projects a .gitignore file is typically quite lengthy. For efficiency and security reasons, there are often quite a few directories and files that should be removed from source control.

GitHub

It’s a good habit to create a remote repository of your code for each project. This way you have a backup in case anything happens to your computer and more importantly, it allows for collaboration with other software developers. Popular choices include GitHub, Bitbucket, and GitLab. When you’re learning web development, it is highly recommended to use private rather than public repositories so you don’t inadvertently post critical information such as passwords online.

We will use GitHub in this book but all three services offer similar functionality for newcomers. Sign up for a free account on GitHub’s homepage and verify your email address. It is also now required to add 2FA (two-factor authentication) for increased security. Once fully signed up navigate to the “Create a new repository” page located at https://github.com/new.

Enter the repository name hello-world and click on the radio button next to “Private” rather than “Public.” Then click on the button at the bottom for “Create Repository.”

Your first repository is now created! However there is no code in it yet. Scroll down on the page to where it says “…or push an existing repository from the command line.” That’s what we want. Copy the text immediately under this headline and paste it into your command line. Here is what it looks like for me with my GitHub username of wsvincent. Your username will be different.

$ git remote add origin https://github.com/wsvincent/hello-world.git
$ git branch -M main
$ git push -u origin main

The first line adds the remote repo on GitHub to our local Git configuration, the next line establishes the default branch as main, and the third line “pushes” the code up to GitHub’s servers. The -u flag creates a tracking reference for every new branch that you successfully push onto the remote repository. The next time we push commits we will only need the command git push origin main.

Assuming everything worked properly, you can now go back to your GitHub webpage and refresh it. Your local code is now hosted online!

SSH Keys

Unfortunately, there is a good chance that the last command yielded an error if you are a new developer and do not have SSH keys already configured.

ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

This cryptic message means we need to configure SSH keys. This is a one-time thing but a bit of a hassle to be honest. The Secure Shell Protocol (SSH) is a protocol used to ensure private connections with a remote server. The process involves generating unique SSH keys and storing them on your computer so only GitHub can access them. For regular websites authentication is done via username/password powered by Hypertext Transfer Protocol Secure (HTTPS), essentially encrypted HTTP. But for very important websites–and your GitHub repos which store all your code count as such–SSH is a more secure approach.

First, check whether you have existing SSH keys. Github has a guide to this that works for Mac, Windows, and Linux. If you don’t have existing public and private keys, you’ll need to generate them. GitHub, again, has a guide on doing this.

Once complete you should be able to execute the git push -u origin main command successfully!

It’s normal to feel overwhelmed and frustrated if you become stuck with SSH keys. GitHub has a lot of resources to walk you through it but the reality is that it’s very intimidating the first time. If you’re truly stuck, continue with the book and come back to SSH Keys and GitHub with a full night’s sleep. I can’t count the number of times a clear head has helped me process a difficult programming issue.

Assuming success with GitHub, go ahead and exit our virtual environment with the deactivate command.

(.venv) $ deactivate

You should no longer see parentheses on your command line, indicating the virtual environment is no longer active.

Conclusion

Congratulations! We’ve covered a lot of fundamental concepts in this chapter. We built our first Django application and learned about Django’s project/app structure. We started to learn about views, urls, and the internal Django web server. And we worked with Git to track our changes and pushed our code into a private repo on GitHub.

If you become stuck at any point, compare your code against the official repo.

Continue on to the next chapter where we’ll build and deploy a more complex Django application using templates and class-based views.