Chapter 2: Hello World app
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.
The full domain name for a website like LearnDjango.com is actually
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.
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
- 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:
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__.pyindicates 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.pyallows for an optional Asynchronous Server Gateway Interface to be run.
settings.pycontrols our Django project’s overall settings
urls.pytells Django which pages to build in response to a browser or URL request
wsgi.pystands for Web Server Gateway Interface, more on this in the next chapter when we do our first deployment
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:
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–
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
(.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.pyis a configuration file for the built-in Django Admin app
apps.pyis a configuration file for the app itself
migrations/keeps track of any changes to our
models.pyfile so it stays in sync with our database
models.pyis where we define our database models which Django automatically translates into database tables
tests.pyis for app-specific tests
views.pyis 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
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.
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
- an optional named URL pattern called
In other words, if the user requests the homepage represented by the empty string
"", Django should use the view called
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 ]
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
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!”
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
(.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
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
Enter to exit before entering this command.
(.venv) $ pip freeze > requirements.txt
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.
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!
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
(.venv) $ deactivate
You should no longer see parentheses on your command line, indicating the virtual environment is no longer active.
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.