Django Fundamentals
Django is a Python tool that helps you build websites faster and with less effort. Instead of writing everything from scratch, Django gives you ready-made pieces you can use right away. It follows a pattern called MVT (Model-View-Template), which is just a way to keep your code organized by splitting it into three parts.
Architecture
Section titled “Architecture”Think of Django like a restaurant:
-
Model - This is the kitchen where all the data lives. It talks to the database (where your information is stored) and knows how to read and save things. In Django, you write models as Python classes.
-
View - This is the waiter. When a user makes a request (like clicking a button), the View decides what to do - it gets the right data from the Model and sends it to the Template.
-
Template - This is the plate the food is served on. It’s the HTML page the user actually sees in their browser. Django uses a simple system to fill in the data before showing it to the user.
Django Folder Structure
Section titled “Django Folder Structure”When you start a new Django project, it automatically creates a bunch of files and folders for you. Here’s what they all mean:
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.pymanage.py- Your main helper tool. You run commands like starting the server or creating the database using this file.myproject/- The main settings folder for your whole project.app1/- A mini-project inside your big project. Each app handles one specific thing (like a blog, a shop, etc.). You can have many apps.migrations/- Keeps track of changes you make to your database over time. Think of it as a history of your database.models.py- Where you describe what your data looks like (e.g. a “Post” has a title and a body).views.py- Where you write the logic for what happens when someone visits a page.admin.py- Lets you manage your data from a nice built-in admin panel.settings.py- The main config file. Database, installed apps, security settings - it all lives here.urls.py- Maps web addresses (like/about/) to the right view function.asgi.pyandwsgi.py- Entry points that let your Django app talk to a web server.
Django Settings
Section titled “Django Settings”The settings.py file controls how your Django project behaves. Here’s how to write one that works well for both development (on your computer) and production (on a real server).
Basic Config and Environment Variables
Section titled “Basic Config and Environment Variables”Instead of putting passwords and secret keys directly in your code (which is unsafe), you store them in a separate .env file. Django reads them from there using environment variables.
First, install the tool that reads .env files:
uv add python-dotenvThen in settings.py:
from pathlib import Pathimport osimport sysimport dotenvfrom django.core.exceptions import ImproperlyConfigured
BASE_DIR = Path(__file__).resolve().parent.parent
# Load values from .env if file existsenv_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 controlDEBUG = os.getenv("DEBUG", "False").lower() == "true"PRODUCTION = "production"ENVIRONMENT = os.getenv("ENVIRONMENT", PRODUCTION).lower()DEBUG = Truemeans you see detailed error pages. Turn it off on a live site.ENVIRONMENTlets you know if you’re running locally or on a real server, so you can behave differently in each case.
Generating a SECRET_KEY
Section titled “Generating a SECRET_KEY”Django needs a secret key to keep things like cookies and sessions secure. Never share it publicly. Generate one like this:
from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())Copy the output and put it in your .env file.
Allowed Hosts
Section titled “Allowed Hosts”This setting tells Django which website addresses are allowed to use your app. It’s a security check - without it, anyone could pretend to be your site.
ALLOWED_HOSTS = [ host.strip().lower() for host in os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")]Every feature you build in Django lives inside an “app”. You need to register each app here so Django knows about it.
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]The ones starting with django.contrib are built-in features Django provides out of the box (like user login, admin panel, etc.).
Middleware
Section titled “Middleware”Middleware is code that runs on every single request before it reaches your view, and on every response before it goes back to the user. Think of it as a series of checkpoints.
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",]Each line is a different security or functionality check. The order matters - they run top to bottom.
Routing Configuration
Section titled “Routing Configuration”This tells Django where to find all your URL rules:
ROOT_URLCONF = "myproject.urls"Replace myproject with your actual project folder name.
WSGI and ASGI
Section titled “WSGI and ASGI”These are just the “doors” that let your Django app connect to a web server. Most beginners don’t need to change this:
WSGI_APPLICATION = "myproject.wsgi.application"Templates
Section titled “Templates”This tells Django where to look for your HTML files:
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", ], }, },]"DIRS": [BASE_DIR / "templates"]- tells Django to look in atemplates/folder in your project root."APP_DIRS": True- also looks inside each app’s owntemplates/folder automatically.
Database
Section titled “Database”Django needs to know where to store your data. For learning and local development, SQLite is perfect (it’s just a file on your computer). For a real website, use something like MySQL.
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", } }By reading DB_ENGINE from the environment, the same settings.py works for both local and production without any code changes.
Static Files
Section titled “Static Files”Static files are things like CSS, JavaScript, and images - files that don’t change based on user data.
First, install WhiteNoise (a tool that helps serve these files efficiently):
uv add whitenoiseAdd it to middleware (it must go right after SecurityMiddleware):
MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", # other middleware ...]
STATIC_URL = "/static/"STATIC_ROOT = BASE_DIR / "staticfiles"STATICFILES_DIRS = [BASE_DIR / "static"]STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"Before deploying, run this command to collect all static files into one folder:
uv run python manage.py collectstatic --noinputWhiteNoise is great because:
- It serves your static files directly from Django - no extra server needed.
- It compresses files so they load faster for users.
Media Files
Section titled “Media Files”Media files are files that users upload (like profile pictures). They’re different from static files because they change over time.
MEDIA_URL = "/media/"MEDIA_ROOT = BASE_DIR / "media"MEDIA_URL- the web address prefix for uploaded files (e.g./media/profile.jpg).MEDIA_ROOT- the folder on your server where uploads are actually saved.
Auth and User Model Settings
Section titled “Auth and User Model Settings”These settings control where Django sends users when they log in or out:
LOGIN_URL = "/accounts/login/" # where to go if not logged inLOGIN_REDIRECT_URL = "/" # where to go after logging inLOGOUT_REDIRECT_URL = "/accounts/login/" # where to go after logging out
AUTH_USER_MODEL = "appname.User" # use a custom user modelIf you’re building an API (using DRF), you can ignore the first three - they’re only for regular HTML websites.
Production Security Settings
Section titled “Production Security Settings”When your site is live, you want extra security turned on. This block only runs when DEBUG is off and you’re in production:
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 # force 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"Each line here closes a common security hole. Don’t worry about memorizing what each one does - just keep this block and it will protect your site.
Logging
Section titled “Logging”Logging means recording what your app is doing. It’s like a diary - when something goes wrong, you check the log to see what happened.
Django lets you send logs to:
- The terminal/console - great while developing
- A log file - great for production so you can check later
Log levels (from least serious to most serious): DEBUG -> INFO -> WARNING -> ERROR -> CRITICAL
If you set the level to INFO, Django only records INFO and above (not debug noise). If you set it to ERROR, you only get serious problems.
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("DJANGO_LOG_LEVEL", "INFO").upper(), }, "appname": { "handlers": ["console", "file", "error_file"], "level": os.getenv("DJANGO_LOG_LEVEL", "INFO").upper(), }, }, "formatters": { "verbose": { "format": "{asctime} ({levelname}) - {name}: {message}", "style": "{", }, "simple": { "format": "{levelname} - {message}", "style": "{", }, },}Here’s what each part does in plain English:
handlers- Where do logs go?console-> prints to your terminal while developingfile-> saves everything tologs/app.logerror_file-> saves only errors tologs/error.log(so serious problems are easy to find)
loggers- Which parts of your app get logged? The empty string""means “everything”. You can also add a specific logger just for your app (replaceappnamewith your app name).formatters- How do the log messages look?verbose-> shows time, level, name, and message (good for files)simple-> shows just level and message (good for terminal)
Project URL Configuration
Section titled “Project URL Configuration”This is the main urls.py for your project. It connects web addresses to the right views.
from django.conf import settingsfrom django.conf.urls.static import staticfrom django.contrib import adminfrom 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)Things to remember:
- Replace
appnamewith your actual app name everywhere. - The
handler404,handler500, etc. let you show a nice custom error page instead of Django’s default ugly ones. - The media
static()line only works in development. On a real server, use Nginx to serve media files.
URL Converters
Section titled “URL Converters”When a URL has a variable part (like a user ID or a post slug), you use path converters to tell Django what type of value to expect.
Format: <converter:variable-name>
| Converter | Type | Examples |
|---|---|---|
<int:id> | Integer | 5, 100, 999 |
<str:name> | String | hello, product-name |
<slug:slug> | URL-safe | hello-world |
<uuid:code> | UUID | 550e8400-… |
Example:
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"),]Django will check that /users/abc/ doesn’t match <int:id> because abc is not a number. This saves you from writing that check yourself.
Django Views
Section titled “Django Views”A view is just a Python function (or class) that:
- Gets a request from the user
- Does some work (fetch data, check login, etc.)
- Returns a response (an HTML page, JSON data, a redirect, etc.)
Django has two styles of views:
- Function-Based Views (FBV) - simple functions
- Class-Based Views (CBV) - organized into classes
Both work great. Use whichever feels clearer to you.
Reading Data from GET and POST Requests
Section titled “Reading Data from GET and POST Requests”When a user visits a URL with filters (like /products/?search=phone), the data after ? is called query params. You read them with request.GET.
When a user submits a form (like a login form), the data is sent as a POST request. You read it with request.POST.
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")
# For repeated values like: /products/?tag=python&tag=django tags = request.GET.getlist("tag")
return JsonResponse( { "search": search, "category": category, "page": page, "tags": tags, } )For a 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")Good habits:
- Always use
.get("key", default)so your code doesn’t crash if the key is missing. - Treat
request.GETandrequest.POSTdata as untrusted - never trust user input directly. - For real forms, use Django Forms (
is_valid()andcleaned_data) for proper validation.
Function-Based Views (FBV)
Section titled “Function-Based Views (FBV)”FBVs are plain Python functions. They’re great when the logic is simple and you want full control.
from django.contrib.auth.decorators import login_requiredfrom django.shortcuts import get_object_or_404, redirect, renderfrom django.http import JsonResponse
from .models import Article
@login_requireddef article_detail(request, slug): article = get_object_or_404(Article, slug=slug)
if request.method == "POST": 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)What’s happening here:
@login_required- blocks the page if the user is not logged inget_object_or_404()- tries to find the article; shows a 404 page if not foundrequest.method == "POST"- separates read (GET) and write (POST) actionsredirect()- sends the user to another page after saving (prevents double-submit on refresh)JsonResponse- returns data as JSON, useful for simple APIs
Use FBVs when:
- The logic is short or unique
- You want to keep things simple and readable
Class-Based Views (CBV)
Section titled “Class-Based Views (CBV)”CBVs group related actions (list, detail, create, etc.) into one class. Django gives you ready-made generic classes so you write less code.
from django.http import HttpResponsefrom 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")Each HTTP method (GET, POST, etc.) becomes its own method in the class.
Generic CBVs (Less code, more power)
Section titled “Generic CBVs (Less code, more power)”from django.contrib.auth.mixins import LoginRequiredMixinfrom django.urls import reverse_lazyfrom 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")ListView- shows a list of items with automatic paginationDetailView- shows one item (looks it up by slug)CreateView- shows a form and saves a new item when submittedLoginRequiredMixin- blocks the page if not logged in (CBV version of@login_required)
Use CBVs when:
- You’re building many similar pages (list, detail, create, edit, delete)
- You want to reuse logic across views using mixins
Decorators
Section titled “Decorators”Decorators are small add-ons you put above a view function to change how it behaves.
@login_required
Section titled “@login_required”Only lets logged-in users see the page:
from django.contrib.auth.decorators import login_required
@login_required(login_url="/accounts/login/")def dashboard(request): return render(request, "accounts/dashboard.html")@csrf_exempt
Section titled “@csrf_exempt”Turns off CSRF protection for one view. Only use this for trusted webhook endpoints:
from django.views.decorators.csrf import csrf_exempt
@csrf_exemptdef webhook_receiver(request): return JsonResponse({"received": True})Don’t use @csrf_exempt on regular pages - it removes an important security check.
Other useful decorators:
@require_http_methods(["GET", "POST"])- only allow specific HTTP methods@require_POST- only allow POST requests@permission_required("app_label.permission_name")- check for a specific permission@cache_page(60 * 5)- cache the page for 5 minutes
Helper Functions
Section titled “Helper Functions”Django gives you a few shortcuts that you’ll use all the time:
render()
Section titled “render()”Fills in an HTML template with data and returns it to the user:
from django.shortcuts import render
def profile(request): return render(request, "accounts/profile.html", {"user": request.user})redirect()
Section titled “redirect()”Sends the user to a different page:
from django.shortcuts import redirect
def go_home(request): return redirect("home")JsonResponse()
Section titled “JsonResponse()”Returns data as JSON (useful for APIs):
from django.http import JsonResponse
def health_check(request): return JsonResponse({"ok": True, "service": "django-app"})get_object_or_404()
Section titled “get_object_or_404()”Finds one object in the database, or shows a 404 page if it doesn’t exist:
from django.shortcuts import get_object_or_404
article = get_object_or_404(Article, slug=slug)This is much cleaner than writing a try/except block yourself.
Connecting URLs to Views
Section titled “Connecting URLs to Views”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"),]Key rule: CBVs need .as_view() at the end. FBVs are passed directly.
Good URL habits:
- Always give your URLs a
name=- you can use it in templates and redirects instead of hardcoding the path. - Use slugs or UUIDs in public URLs instead of database IDs for better security and readability.
Security
Section titled “Security”CORS (Cross-Origin Resource Sharing)
Section titled “CORS (Cross-Origin Resource Sharing)”CORS is a browser security feature. By default, browsers block JavaScript from one website from talking to a different website’s API. CORS rules let you decide which websites are allowed to call your Django API.
For example, if your frontend is on http://localhost:3000 and your Django API is on http://localhost:8000, without CORS the browser will block the request.
-
Install the package:
Terminal window uv add django-cors-headers -
Add it to your
INSTALLED_APPSandMIDDLEWAREinsettings.py:INSTALLED_APPS = [# other apps"corsheaders",]MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", # must be high in the list# other middleware] -
Tell Django which origins (websites) are allowed to call your API:
CORS_ALLOWED_ORIGINS = ["http://localhost:3000","http://127.0.0.1:3000",]