Skip to content

Django Fundamentals

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It follows the Model-View-Template (MVT) architectural pattern, which is a variant of the traditional Model-View-Controller (MVC) pattern. Django provides a comprehensive set of tools and features that make it easier to build robust and scalable web applications.

Django’s architecture is based on the MVT pattern, which consists of three main components:

  1. Model: The Model is responsible for managing the data of the application. It defines the structure of the database and provides an interface for interacting with the data. In Django, models are defined as Python classes that inherit from django.db.models.Model.

  2. View: The View is responsible for handling user requests and returning responses. It processes the data from the Model and renders it using templates. In Django, views are defined as Python functions or classes that take a web request and return a web response.

  3. Template: The Template is responsible for presenting the data to the user. It defines the structure and layout of the HTML pages that are rendered by the views. In Django, templates are defined using a simple templating language that allows for dynamic content generation.

graph TD Model["Model<br/>(Data Management)"] -->|Interacts with| View["View<br/>(Request Handling)"] View -->|Renders| Template["Template<br/>(Presentation)"] Template -->|Displays| User["User Interface"]

When you create a new Django project, it generates a specific folder structure that organizes the different components of the application. The main folders and files in a Django project include:

myproject/
├── manage.py
├── myproject/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
└── app1/
├── migrations/
│ ├── __init__.py
│ └── 0001_initial.py
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── form.py
├── tests.py
└── views.py
  • manage.py: A command-line utility that allows you to interact with your Django project. You can use it to run the development server, create database migrations, and perform other administrative tasks.
  • myproject/: The main project folder that contains the settings and configuration for the entire project.
  • app1/: A Django application folder that contains the code for a specific functionality of the project. You can have multiple applications within a Django project, each responsible for a different aspect of the application.
  • migrations/: A folder that contains database migration files, which are used to manage changes to the database schema over time.
  • models.py: A file where you define the data models for your application.
  • views.py: A file where you define the views that handle user requests and return responses.
  • admin.py: A file where you can register your models with the Django admin site, allowing you to manage your data through a web interface.
  • settings.py: A file that contains the configuration settings for your Django project, such as database settings, installed applications, and middleware.
  • urls.py: A file where you define the URL patterns for your application, mapping URLs to views.
  • asgi.py and wsgi.py: Files that serve as entry points for ASGI and WSGI servers, respectively, allowing your Django application to communicate with web servers.

This section shows how to write a production-ready settings.py for a Django project named myproject. It uses environment variables, safe defaults, and clear production checks.

Using These environment variables allows you to keep sensitive information out of your codebase and easily switch between development and production settings without changing the code.

Install python-dotenv to load environment variables from a .env file in development:

Terminal window
uv add python-dotenv

In the settings.py, load environment variables and set up basic settings:

from pathlib import Path
import os
import sys
import dotenv
from django.core.exceptions import ImproperlyConfigured
BASE_DIR = Path(__file__).resolve().parent.parent
# Load values from .env if file exists
env_path = BASE_DIR / ".env"
if env_path.exists():
dotenv.load_dotenv(env_path)
SECRET_KEY = os.getenv("SECRET_KEY")
if not SECRET_KEY:
raise ImproperlyConfigured("SECRET_KEY not set")
# Keep this exact pattern for clean environment control
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
PRODUCTION = "production"
ENVIRONMENT = os.getenv("ENVIRONMENT", PRODUCTION).lower()

Why this check is needed:

  • DEBUG lets you avoid dev behavior in production.
  • ENVIRONMENT lets you switch safely between production and non-production.

This is a critical setting for security. In production, always set SECRET_KEY as an environment variable. For local development, you can generate one with this code:

from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())

Using Allowed Hosts is a security risk. Always set ALLOWED_HOSTS in production to the specific domains your app will serve.

ALLOWED_HOSTS = [
host.strip().lower()
for host in os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
]

App is a Django component that encapsulates a specific functionality of the project. It can be reused across different projects and can be easily plugged into the main project. In settings.py, you need to list all the apps that are part of your project in the INSTALLED_APPS setting. This includes both built-in Django apps and any custom apps you create.

INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"accounts.apps.AccountsConfig",
"third_party_app", # add your third-party apps here
"add_custom_apps_here", # add your custom apps here
]

Middleware is a way to process requests and responses globally before they reach the view or after the view has processed them. In settings.py, you need to list all the middleware components that your project will use in the MIDDLEWARE setting. This includes both built-in Django middleware and any custom middleware you create.

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # serve static files efficiently
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

Route configuration defines what should return when a user accesses a specific URL. In settings.py, you need to specify the root URL configuration module in the ROOT_URLCONF setting. This module contains the URL patterns that map URLs to views.

ROOT_URLCONF = "myproject.urls"

WSGI (Web Server Gateway Interface) and ASGI (Asynchronous Server Gateway Interface) are interfaces that allow your Django application to communicate with web servers. WSGI is the traditional interface for synchronous applications, while ASGI is designed for asynchronous applications and supports features like WebSockets.

WSGI_APPLICATION = "myproject.wsgi.application"

Templates are used to render HTML pages in Django. In settings.py, you need to configure the template settings in the TEMPLATES setting. This includes specifying the template engine, directories where templates are located, and context processors.

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]

Database Configration help us to connect our Django application to a database. In settings.py, you need to configure the database settings in the DATABASES setting. This includes specifying the database engine, name, user, password, host, and port.

Use if/else so one settings file works for local and production:

  • local dev can use SQLite quickly
  • production can use MySQL for better scaling
DB_ENGINE = os.getenv("DB_ENGINE", "sqlite")
if DB_ENGINE == "mysql":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": os.getenv("DB_NAME"),
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": os.getenv("DB_HOST"),
"PORT": os.getenv("DB_PORT", "3306"),
"OPTIONS": {"charset": "utf8mb4"},
}
}
else:
DATA_DIR = BASE_DIR / "_data"
DATA_DIR.mkdir(exist_ok=True)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": DATA_DIR / "db.sqlite3",
}
}

Static files are files that are served directly to the client, such as CSS, JavaScript, images, and fonts. In settings.py, you need to configure the static file settings in the STATIC_URL, STATIC_ROOT, and STATICFILES_DIRS settings. This includes specifying the URL prefix for static files, the directory where static files will be collected for production, and any additional directories where static files are located.

Install WhiteNoise:

Terminal window
uv add whitenoise

Use WhiteNoise to serve static files efficiently in production. Add it to the MIDDLEWARE setting after SecurityMiddleware.

MIDDLEWARE = [
# other middleware ...
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# whitenoise must be after SecurityMiddleware
# other middleware ...
]
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

Why we use WhiteNoise:

  • serves static files directly from Django app
  • no separate static server needed for simple deployments
  • supports compressed and hashed files for faster loading and cache safety

Also run this before deployment:

Terminal window
uv run python manage.py collectstatic --noinput

Media files are user-uploaded files that need to be served separately from static files. In settings.py, you need to configure the media file settings in the MEDIA_URL and MEDIA_ROOT settings. This includes specifying the URL prefix for media files and the directory where media files will be stored.

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

By the help of these setting we can customize the authentication system of our Django application. In settings.py, you can specify the login URL, redirect URLs after login and logout, and the custom user model if you have one.

LOGIN_URL = "/accounts/login/" #(not needed in DRF)
LOGIN_REDIRECT_URL = "/" #(not needed in DRF)
LOGOUT_REDIRECT_URL = "/accounts/login/" #(not needed in DRF)
AUTH_USER_MODEL = "appname.User"

These are turned on only in real production. This keeps development easy while ensuring production is secure by default. By the help of these settings we can enhance the security of our Django application in production environments. In settings.py, you can specify various security settings such as CSRF protection, secure cookies, HTTP headers, and SSL settings.

if not DEBUG and ENVIRONMENT == PRODUCTION:
CSRF_TRUSTED_ORIGINS = [
origin for origin in os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",") if origin
]
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000 # HTTPS for 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_SAVE_EVERY_REQUEST = True
X_FRAME_OPTIONS = "DENY"

Logging is an important part of building and maintaining a Django application. It helps you see what your app is doing, diagnose problems quickly, and keep a history of important events. In settings.py, Django lets you control logging through the LOGGING setting.

With LOGGING, you can decide:

  • which messages should be recorded
  • where those messages should be sent
  • how each message should look

Django logging is usually built from three parts:

  • a log level, which decides how important a message must be before it is recorded
  • a log handler, which decides where the message goes
  • a formatter, which decides how the message is displayed

Common handlers include StreamHandler, which writes messages to the console, and FileHandler, which writes messages to a file. You can also create custom handlers when your project needs more advanced behavior.

  • StreamHandler is best for local development because it shows log messages immediately in the terminal or server console.
  • FileHandler is useful in production because it stores logs in a file that you can review later for debugging, auditing, or troubleshooting.

Django also supports several log levels, including DEBUG, INFO, WARNING, ERROR, and CRITICAL. A lower level records more detail, while a higher level records only more serious events. This helps you control how much information is collected in different environments.

LOG_DIR = BASE_DIR / "logs"
LOG_DIR.mkdir(exist_ok=True)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
"file": {
"class": "logging.FileHandler",
"filename": LOG_DIR / "app.log",
"formatter": "verbose",
},
"error_file": {
"class": "logging.FileHandler",
"filename": LOG_DIR / "error.log",
"formatter": "verbose",
"level": "ERROR",
},
},
"loggers": {
"": {
"handlers": ["console", "file", "error_file"],
"level": os.getenv.get("DJANGO_LOG_LEVEL", "INFO").upper(),
},
# otionally add app-specific loggers for more granular control
"appname": {
"handlers": ["console", "file", "error_file"],
"level": os.getenv.get("DJANGO_LOG_LEVEL", "INFO").upper(),"
},
},
"formatters": {
"verbose": {
# docs: https://docs.python.org/3/library/logging.html#logrecord-attributes
# this docs page lists all the available attributes you can use in log formatting
"format": "{asctime} ({levelname}) - {name}: {message}",
"style": "{", # `{` use str.format() style, `%` use old printf style, `$` use string.Template style
},
"simple": {
"format": "{levelname} - {message}",
"style": "{",
},
},
}
  • version: This is the schema version for the logging configuration. Django expects this to be set to 1.
  • disable_existing_loggers: When set to True, Django turns off loggers that were already configured elsewhere. Keeping it False is usually safer because it preserves existing logging behavior.
  • handlers: This section defines where log messages go. In this example, there are three handlers: console, file, and error_file.
    • console: Sends log output to the console so you can watch application activity while developing.
      • class: Tells Django which handler implementation to use. Here, logging.StreamHandler writes messages to standard output.
      • formatter: Specifies which formatter to use for this handler. In this case, it uses the simple formatter defined later in the configuration.
    • file: Saves general log messages to a file.
      • class: Uses logging.FileHandler to write messages to disk.
      • filename: Points to the file that will store the logs. Here, LOG_DIR / "app.log" means the file will be created inside the logs folder as app.log.
      • formatter: Uses the verbose formatter for more detailed log messages.
    • error_file: Stores only error-level messages and more severe problems in a separate file.
      • class: Also uses logging.FileHandler.
      • filename: Points to the dedicated error log file. In this example, it is LOG_DIR / "error.log".
      • formatter: Uses the verbose formatter for more detailed error messages.
      • level: This setting means that only messages with a severity of ERROR or higher will be recorded by this handler. This helps keep the error log focused on critical issues without being cluttered by less important messages.
  • loggers: This section defines which handlers should be used for different loggers. The example includes the root logger and an application-specific logger named appname.
    • "" (root logger): This is the default logger. It captures log messages that do not belong to a more specific logger and sends them to the console and both log files.
    • appname: This logger is useful when you want separate logging behavior for one part of your project. You can give it its own handlers or level if needed.
    • level: This sets the minimum severity a message must have before the logger records it. The example reads the level from the DJANGO_LOG_LEVEL environment variable, and falls back to INFO when the variable is not set.
  • formatters: This optional section controls how each log message is displayed.
    • verbose: This formatter creates a detailed message format that includes the time, log level, logger name, and the actual message. The style value of { means the format string uses Python’s str.format() syntax.
    • simple: This formatter creates a more concise message format that includes only the log level and the message. It also uses str.format() syntax.

This is the main urls.py pattern for a Django project. For example, if your project folder is myproject, then write myproject.views.

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from . import views
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("appname.urls")),
]
handler400 = "appname.views.error_400_view"
handler403 = "appname.views.error_403_view"
handler404 = "appname.views.error_404_view"
handler500 = "appname.views.error_500_view"
if settings.ENVIRONMENT != settings.PRODUCTION:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Notes:

  • Replace appname with your Django project name.
  • Keep custom error handlers in the main project urls.py.
  • Serve media files with static() only in development.
  • Do not use this media setup for production servers.

Path converters let Django validate URL segments before your view runs. This makes routes safer, cleaner, and easier to maintain.

<converter:variable-name> i.e. int, str, slug, uuid, etc. are called path converters. They ensure the URL segment matches the expected format before calling the view. And variable-name is the name of the parameter passed to the view like id, slug, etc.

ConverterTypeExamples
<int:id>Integer5, 100, 999
<str:name>Stringhello, product-name
<slug:slug>URL-safehello-world
<uuid:code>UUID550e8400-…

Example usage in urls.py:

from django.urls import path
from . import views
urlpatterns = [
path("users/<int:id>/", views.user_detail, name="user-detail"),
path("categories/<str:name>/", views.category_detail, name="category-detail"),
path("posts/<slug:slug>/", views.post_detail, name="post-detail"),
path("invoices/<uuid:code>/", views.invoice_detail, name="invoice-detail"),
]

In Django, a view is the code that receives an HTTP request and returns an HTTP response. Views are where request handling decisions happen, such as:

  • reading URL parameters
  • validating request method (GET, POST, etc.)
  • fetching data from models
  • returning HTML, JSON, redirects, or errors

Django supports two main styles:

  • Function-Based Views (FBV)
  • Class-Based Views (CBV)

Both are production-ready. Use the style that keeps the code easiest to read and maintain for your team.

This is one of the most important basics in Django views.

  • Use request.GET for query string data (URL params after ?)
  • Use request.POST for form body data sent with POST requests

Example URL:

/products/?search=phone&category=electronics&page=2

def product_list(request):
search = request.GET.get("search", "")
category = request.GET.get("category")
page = request.GET.get("page", "1")
# Example: /products/?tag=python&tag=django
tags = request.GET.getlist("tag")
return JsonResponse(
{
"search": search,
"category": category,
"page": page,
"tags": tags,
}
)

For POST form submission:

from django.shortcuts import redirect, render
def contact_submit(request):
if request.method == "POST":
name = request.POST.get("name", "").strip()
email = request.POST.get("email", "").strip().lower()
message = request.POST.get("message", "").strip()
if not name or not email:
return render(request, "contact.html", {"error": "Name and email are required."})
# save/send data
return redirect("contact-success")
return render(request, "contact.html")

Best practices:

  • Use .get("key", default) to avoid KeyError
  • Use .getlist("key") for repeated params
  • Treat request.GET and request.POST as untrusted input
  • For real forms, prefer Django Forms and cleaned_data after is_valid()

Function-Based Views are plain Python functions. They are usually the best choice when logic is custom and straightforward.

from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from django.http import JsonResponse
from .models import Article
@login_required
def article_detail(request, slug):
article = get_object_or_404(Article, slug=slug)
if request.method == "POST":
# Example: update a counter or process form data
article.views_count += 1
article.save(update_fields=["views_count"])
return redirect("article-detail", slug=article.slug)
context = {"article": article}
return render(request, "articles/detail.html", context)
def article_api(request):
data = {
"total_articles": Article.objects.count(),
"status": "ok",
}
return JsonResponse(data, status=200)

How to think about this example:

  • @login_required protects the page so only logged-in users can access it.
  • get_object_or_404() returns the article or an automatic 404 response.
  • request.method == "POST" keeps write logic explicit and easy to audit.
  • redirect() prevents form re-submission on browser refresh.
  • JsonResponse is clean for simple API-style endpoints.

Use FBV when:

  • logic is short or highly custom
  • you want full control over each method branch
  • clarity is more important than abstraction

Production notes for FBV:

  • Keep one view focused on one job.
  • Check methods explicitly for write operations.
  • Apply auth and permission decorators close to the view.
  • Move repeated logic into helper functions or services.

Class-Based Views group related behavior in classes and provide reusable generic views. They reduce repeated code for common CRUD pages.

from django.http import HttpResponse
from django.views import View
class HelloView(View):
def get(self, request):
return HttpResponse("Hello from GET")
def post(self, request):
return HttpResponse("Hello from POST")

This pattern maps HTTP methods (get, post, and so on) to class methods.

from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, DetailView, ListView
from .models import Article
class ArticleListView(ListView):
model = Article
template_name = "articles/list.html"
context_object_name = "articles"
paginate_by = 10
class ArticleDetailView(DetailView):
model = Article
template_name = "articles/detail.html"
context_object_name = "article"
slug_field = "slug"
slug_url_kwarg = "slug"
class ArticleCreateView(LoginRequiredMixin, CreateView):
model = Article
fields = ["title", "slug", "content"]
template_name = "articles/form.html"
success_url = reverse_lazy("article-list")

How to think about these classes:

  • ListView handles listing and pagination quickly.
  • DetailView handles single-object lookup by slug.
  • CreateView handles form display, validation, and object creation.
  • LoginRequiredMixin protects create actions without repeating auth code.

Use CBV when:

  • many pages follow similar CRUD patterns
  • you want to reuse behavior through mixins
  • you want less boilerplate around forms and object lookup

Production notes for CBV:

  • Put auth mixins before the generic view class in inheritance order.
  • Override only what you need (get_queryset, form_valid, etc.).
  • Keep business logic out of templates and large view methods.

Use this when only authenticated users should access a view. It is the first protection layer for private pages.

from django.contrib.auth.decorators import login_required
@login_required(login_url="/accounts/login/")
def dashboard(request):
return render(request, "accounts/dashboard.html")

This disables CSRF protection for a view. Use it only when absolutely required, such as a trusted webhook endpoint you cannot protect with CSRF tokens.

from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook_receiver(request):
return JsonResponse({"received": True})

Production note:

  • Avoid @csrf_exempt for regular browser form endpoints.
  • For APIs, prefer token/session strategies designed for your client type.

Other useful decorators:

  • @require_http_methods(["GET", "POST"])
  • @require_POST
  • @permission_required("app_label.permission_name")
  • @cache_page(60 * 5)

render(request, template_name, context=None, status=200)

Section titled “render(request, template_name, context=None, status=200)”
  • Combines a template with context and returns HttpResponse
  • Standard choice for server-rendered HTML pages
from django.shortcuts import render
def profile(request):
return render(request, "accounts/profile.html", {"user": request.user})
  • Returns an HTTP redirect (302 by default)
  • to can be a URL name, model object, or absolute path
from django.shortcuts import redirect
def go_home(request):
return redirect("home")
  • Returns JSON output, commonly for API endpoints
  • safe=True means the top-level value must be a dictionary
from django.http import JsonResponse
def health_check(request):
return JsonResponse({"ok": True, "service": "django-app"})
  • Fetches one object or raises Http404
  • Cleaner and safer than manual try/except for missing objects
from django.shortcuts import get_object_or_404
article = get_object_or_404(Article, slug=slug)
from django.urls import path
from .views import ArticleDetailView, ArticleListView, article_detail
urlpatterns = [
path("articles/", ArticleListView.as_view(), name="article-list"),
path("articles/<slug:slug>/", ArticleDetailView.as_view(), name="article-detail"),
path("legacy-article/<slug:slug>/", article_detail, name="legacy-article-detail"),
]

Remember:

  • CBV must be added with .as_view() in urls.py
  • FBV is passed directly as a function

Production notes for URL design:

  • Use clear and stable path names (name="article-detail") for reverse URL lookup.
  • Keep URL names consistent across templates, redirects, and tests.
  • Prefer slug or UUID routes for public resources instead of database IDs when possible.

CORS is a security feature implemented by browsers to restrict web pages from making requests to a different domain than the one that served the web page. This is important to prevent malicious websites from accessing sensitive data on another site without permission.

To enable CORS in a Django application, you can use the django-cors-headers package. This package allows you to specify which origins are allowed to make cross-origin requests to your Django application.

  1. Install the package using uv:

    Terminal window
    uv add django-cors-headers
  2. Then, add it to your INSTALLED_APPS and MIDDLEWARE in settings.py:

    INSTALLED_APPS = [
    # other apps
    "corsheaders",
    ]
    MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware", # must be high in the list
    # other middleware
    ]
  3. You can configure allowed origins in settings.py:

    CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000",
    ]