Django Fundamentals: Models, Views, and Templates (MTV Architecture)
Django is a powerful, batteries-included web framework that follows the MTV (Model-View-Template) architectural pattern. Unlike Flask’s minimalist approach, Django provides a complete ecosystem for building robust web applications with built-in features like authentication, admin panels, and an ORM.
Understanding Django’s MTV architecture is fundamental to building effective Django applications. In this guide, we’ll explore each component in depth, see how they interact, and build practical examples that demonstrate real-world usage patterns.
Understanding Django’s MTV Architecture
MTV vs MVC
Django uses MTV (Model-View-Template) instead of the traditional MVC (Model-View-Controller) pattern:
- Model: Database layer that defines your data structure
- View: Business logic layer that processes requests and returns responses
- Template: Presentation layer that renders HTML to the user
The key difference from MVC is that Django’s View handles what MVC calls both the “Controller” and “View” logic, while Django’s Template is purely for presentation.
The Request-Response Cycle
Understanding how these components work together is crucial:
1. User Request โ 2. URL Router โ 3. View (Business Logic) โ
4. Model (Database) โ 5. Template (HTML Rendering) โ 6. Response to User
When a user visits a URL, Django routes the request to a view, which may query the database through models, then renders a template with the data, and returns the response.
Part 1: Models - Your Data Layer
Models define the structure of your data and how it’s stored in the database. Django’s ORM (Object-Relational Mapping) abstracts away SQL, allowing you to work with Python objects instead.
Creating Your First Model
Models are defined as Python classes in models.py:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Meta:
ordering = ['-created_at']
Field Types
Django provides various field types for different data:
from django.db import models
class BlogPost(models.Model):
# Text fields
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
# Numeric fields
views = models.IntegerField(default=0)
rating = models.FloatField(default=0.0)
# Date/Time fields
published_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Boolean field
is_published = models.BooleanField(default=False)
# Choice field
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
# File fields
featured_image = models.ImageField(upload_to='blog_images/')
pdf_file = models.FileField(upload_to='documents/')
# Email field
author_email = models.EmailField()
# URL field
external_link = models.URLField(blank=True)
Model Relationships
Models often relate to each other. Django supports three types of relationships:
One-to-Many (ForeignKey)
class Author(models.Model):
name = models.CharField(max_length=100)
class BlogPost(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# on_delete=models.CASCADE means if author is deleted, posts are too
Many-to-Many
class Tag(models.Model):
name = models.CharField(max_length=50)
class BlogPost(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag)
One-to-One
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField()
profile_picture = models.ImageField()
Model Methods and Properties
Add custom methods to your models:
from django.db import models
from django.utils import timezone
class BlogPost(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
published_at = models.DateTimeField(null=True, blank=True)
def publish(self):
"""Publish the blog post"""
self.published_at = timezone.now()
self.save()
def is_published(self):
"""Check if post is published"""
return self.published_at is not None
def get_word_count(self):
"""Calculate word count"""
return len(self.content.split())
def __str__(self):
return self.title
Migrations - Managing Database Changes
Migrations track changes to your models:
# Create migrations for changes
python manage.py makemigrations
# Apply migrations to database
python manage.py migrate
# See migration status
python manage.py showmigrations
# Revert to previous migration
python manage.py migrate app_name 0001
A migration file looks like:
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='blogpost',
name='featured_image',
field=models.ImageField(upload_to='images/', null=True),
),
]
Querying Models
The ORM allows you to query models using Python:
from myapp.models import BlogPost, Author
# Get all posts
all_posts = BlogPost.objects.all()
# Filter posts
published_posts = BlogPost.objects.filter(is_published=True)
recent_posts = BlogPost.objects.filter(published_at__gte='2025-01-01')
# Get single object
post = BlogPost.objects.get(id=1)
# Count objects
post_count = BlogPost.objects.count()
# Order results
posts = BlogPost.objects.all().order_by('-published_at')
# Limit results
first_five = BlogPost.objects.all()[:5]
# Complex queries
from django.db.models import Q
posts = BlogPost.objects.filter(
Q(title__icontains='django') | Q(content__icontains='django')
)
# Relationships
author = Author.objects.get(id=1)
author_posts = author.blogpost_set.all()
Part 2: Views - Your Business Logic Layer
Views handle the request processing and return responses. Django supports two approaches: function-based views (FBV) and class-based views (CBV).
Function-Based Views
Simple, straightforward views for basic tasks:
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from myapp.models import BlogPost
def blog_list(request):
"""Display list of blog posts"""
posts = BlogPost.objects.filter(is_published=True).order_by('-published_at')
context = {'posts': posts}
return render(request, 'blog/post_list.html', context)
def blog_detail(request, post_id):
"""Display single blog post"""
post = get_object_or_404(BlogPost, id=post_id, is_published=True)
context = {'post': post}
return render(request, 'blog/post_detail.html', context)
def search_posts(request):
"""Search blog posts"""
query = request.GET.get('q', '')
if query:
posts = BlogPost.objects.filter(
title__icontains=query,
is_published=True
)
else:
posts = []
context = {'posts': posts, 'query': query}
return render(request, 'blog/search_results.html', context)
Class-Based Views
More powerful and reusable for complex views:
from django.views import View
from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from myapp.models import BlogPost
class BlogListView(ListView):
"""Display list of blog posts"""
model = BlogPost
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return BlogPost.objects.filter(is_published=True).order_by('-published_at')
class BlogDetailView(DetailView):
"""Display single blog post"""
model = BlogPost
template_name = 'blog/post_detail.html'
context_object_name = 'post'
pk_url_kwarg = 'post_id'
def get_queryset(self):
return BlogPost.objects.filter(is_published=True)
class CreateBlogPostView(LoginRequiredMixin, CreateView):
"""Create new blog post (requires login)"""
model = BlogPost
template_name = 'blog/post_form.html'
fields = ['title', 'content', 'featured_image']
success_url = reverse_lazy('blog:post_list')
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
Handling Different HTTP Methods
from django.views import View
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator
# Function-based view with multiple methods
@require_http_methods(["GET", "POST"])
def handle_post(request):
if request.method == 'GET':
return render(request, 'post_form.html')
elif request.method == 'POST':
# Process form submission
title = request.POST.get('title')
content = request.POST.get('content')
post = BlogPost.objects.create(title=title, content=content)
return redirect('blog:post_detail', post_id=post.id)
# Class-based view with multiple methods
class PostAPIView(View):
def get(self, request, post_id):
post = get_object_or_404(BlogPost, id=post_id)
return JsonResponse({
'id': post.id,
'title': post.title,
'content': post.content
})
def post(self, request):
data = json.loads(request.body)
post = BlogPost.objects.create(
title=data['title'],
content=data['content']
)
return JsonResponse({'id': post.id}, status=201)
URL Routing
Connect views to URLs in urls.py:
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
# Function-based views
path('', views.blog_list, name='post_list'),
path('post/<int:post_id>/', views.blog_detail, name='post_detail'),
path('search/', views.search_posts, name='search'),
# Class-based views
path('posts/', views.BlogListView.as_view(), name='post_list_cbv'),
path('posts/<int:post_id>/', views.BlogDetailView.as_view(), name='post_detail_cbv'),
path('create/', views.CreateBlogPostView.as_view(), name='create_post'),
]
Part 3: Templates - Your Presentation Layer
Templates render HTML with dynamic content using Django’s Template Language (DTL).
Basic Template Syntax
<!-- Display variables -->
<h1>{{ post.title }}</h1>
<p>By {{ post.author.name }}</p>
<!-- Filters -->
<p>Published: {{ post.published_at|date:"F j, Y" }}</p>
<p>{{ post.content|truncatewords:50 }}</p>
<!-- Conditionals -->
{% if post.is_published %}
<span class="badge">Published</span>
{% else %}
<span class="badge">Draft</span>
{% endif %}
<!-- Loops -->
<ul>
{% for tag in post.tags.all %}
<li>{{ tag.name }}</li>
{% empty %}
<li>No tags</li>
{% endfor %}
</ul>
Template Inheritance
Create a base template that other templates extend:
templates/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Blog{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<nav>
<a href="{% url 'blog:post_list' %}">Home</a>
<a href="{% url 'blog:search' %}">Search</a>
{% if user.is_authenticated %}
<a href="{% url 'blog:create_post' %}">Create Post</a>
<a href="{% url 'logout' %}">Logout</a>
{% else %}
<a href="{% url 'login' %}">Login</a>
{% endif %}
</nav>
<main>
{% block content %}
Default content
{% endblock %}
</main>
<footer>
<p>© 2025 My Blog</p>
</footer>
</body>
</html>
templates/blog/post_list.html:
{% extends "base.html" %}
{% block title %}Blog Posts{% endblock %}
{% block content %}
<h1>Blog Posts</h1>
{% for post in posts %}
<article>
<h2><a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a></h2>
<p class="meta">By {{ post.author.name }} on {{ post.published_at|date:"F j, Y" }}</p>
<p>{{ post.content|truncatewords:30 }}</p>
<a href="{% url 'blog:post_detail' post.id %}">Read more</a>
</article>
{% empty %}
<p>No posts found.</p>
{% endfor %}
{% endblock %}
Common Template Tags and Filters
<!-- URL reversal -->
<a href="{% url 'blog:post_detail' post.id %}">View Post</a>
<!-- Static files -->
<img src="{% static 'images/logo.png' %}" alt="Logo">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<!-- Conditionals -->
{% if user.is_authenticated %}
<p>Welcome, {{ user.username }}!</p>
{% endif %}
<!-- Loops with counter -->
{% for item in items %}
<p>{{ forloop.counter }}: {{ item }}</p>
{% endfor %}
<!-- Filters -->
<p>{{ text|upper }}</p>
<p>{{ text|lower }}</p>
<p>{{ text|title }}</p>
<p>{{ text|truncatewords:10 }}</p>
<p>{{ date|date:"Y-m-d" }}</p>
<p>{{ price|floatformat:2 }}</p>
<!-- Default value -->
<p>{{ author.bio|default:"No bio available" }}</p>
<!-- Pluralize -->
<p>You have {{ count }} comment{{ count|pluralize }}</p>
<!-- Join -->
<p>{{ tags|join:", " }}</p>
Passing Context to Templates
from django.shortcuts import render
from myapp.models import BlogPost, Author
def blog_dashboard(request):
posts = BlogPost.objects.all()
authors = Author.objects.all()
stats = {
'total_posts': posts.count(),
'published_posts': posts.filter(is_published=True).count(),
}
context = {
'posts': posts,
'authors': authors,
'stats': stats,
'user_name': request.user.username,
}
return render(request, 'blog/dashboard.html', context)
templates/blog/dashboard.html:
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<div class="stats">
<p>Total Posts: {{ stats.total_posts }}</p>
<p>Published: {{ stats.published_posts }}</p>
</div>
<h2>Recent Posts</h2>
<ul>
{% for post in posts|slice:":5" %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
<h2>Authors</h2>
<ul>
{% for author in authors %}
<li>{{ author.name }} ({{ author.blogpost_set.count }} posts)</li>
{% endfor %}
</ul>
{% endblock %}
How MTV Components Work Together
Let’s trace a complete request-response cycle:
# 1. User visits: /blog/posts/1/
# 2. urls.py routes to view
path('posts/<int:post_id>/', views.BlogDetailView.as_view(), name='post_detail')
# 3. View processes request
class BlogDetailView(DetailView):
model = BlogPost
template_name = 'blog/post_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
post = self.get_object()
context['related_posts'] = BlogPost.objects.filter(
tags__in=post.tags.all()
).exclude(id=post.id)[:3]
return context
# 4. Model queries database
post = BlogPost.objects.get(id=1)
related_posts = BlogPost.objects.filter(tags__in=post.tags.all())
# 5. Template renders with context
# templates/blog/post_detail.html
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<h3>Related Posts</h3>
{% for related in related_posts %}
<a href="{% url 'blog:post_detail' related.id %}">{{ related.title }}</a>
{% endfor %}
# 6. HTML response sent to user
Best Practices
Model Best Practices
# โ Good: Descriptive names and docstrings
class BlogPost(models.Model):
"""Represents a blog post"""
title = models.CharField(max_length=200, help_text="Post title")
slug = models.SlugField(unique=True)
def __str__(self):
return self.title
class Meta:
ordering = ['-created_at']
verbose_name_plural = "Blog Posts"
# โ Bad: Vague names and no documentation
class BP(models.Model):
t = models.CharField(max_length=200)
View Best Practices
# โ Good: Use class-based views for reusability
class BlogListView(ListView):
model = BlogPost
paginate_by = 10
# โ Good: Keep views focused and simple
def get_blog_posts(request):
posts = BlogPost.objects.filter(is_published=True)
return render(request, 'blog/list.html', {'posts': posts})
# โ Bad: Complex logic in views
def complex_view(request):
# Multiple database queries
# Complex calculations
# Multiple template renders
# Too much responsibility
Template Best Practices
<!-- โ Good: Use template inheritance -->
{% extends "base.html" %}
{% block content %}
<!-- Content here -->
{% endblock %}
<!-- โ Good: Use named URLs -->
<a href="{% url 'blog:post_detail' post.id %}">View</a>
<!-- โ Bad: Hardcoded URLs -->
<a href="/blog/post/{{ post.id }}/">View</a>
<!-- โ Bad: Complex logic in templates -->
{% if user.is_authenticated and user.profile.is_premium and post.is_published %}
<!-- This logic belongs in the view -->
{% endif %}
Project Structure
myproject/
โโโ manage.py
โโโ myproject/
โ โโโ settings.py
โ โโโ urls.py
โ โโโ wsgi.py
โโโ blog/
โ โโโ migrations/
โ โโโ templates/
โ โ โโโ blog/
โ โ โโโ post_list.html
โ โ โโโ post_detail.html
โ โโโ static/
โ โ โโโ blog/
โ โ โโโ css/
โ โโโ models.py
โ โโโ views.py
โ โโโ urls.py
โ โโโ forms.py
โ โโโ admin.py
โโโ templates/
โโโ base.html
Common Pitfalls and Solutions
N+1 Query Problem
# โ Bad: Creates N+1 queries
posts = BlogPost.objects.all()
for post in posts:
print(post.author.name) # Query for each post
# โ Good: Use select_related for ForeignKey
posts = BlogPost.objects.select_related('author')
for post in posts:
print(post.author.name) # No additional queries
# โ Good: Use prefetch_related for ManyToMany
posts = BlogPost.objects.prefetch_related('tags')
for post in posts:
for tag in post.tags.all(): # No additional queries
print(tag.name)
Overloading Templates with Logic
# โ Bad: Complex logic in template
{% if user.is_authenticated and user.profile.subscription_level == 'premium' and post.is_premium %}
<p>Premium content</p>
{% endif %}
# โ Good: Move logic to view
def post_detail(request, post_id):
post = get_object_or_404(BlogPost, id=post_id)
can_view = request.user.is_authenticated and \
request.user.profile.subscription_level == 'premium' and \
post.is_premium
return render(request, 'blog/post_detail.html', {
'post': post,
'can_view': can_view
})
# Template becomes simple
{% if can_view %}
<p>Premium content</p>
{% endif %}
Forgetting to Use get_object_or_404
# โ Bad: Returns 500 error if post doesn't exist
def post_detail(request, post_id):
post = BlogPost.objects.get(id=post_id)
return render(request, 'blog/post_detail.html', {'post': post})
# โ Good: Returns 404 error if post doesn't exist
from django.shortcuts import get_object_or_404
def post_detail(request, post_id):
post = get_object_or_404(BlogPost, id=post_id)
return render(request, 'blog/post_detail.html', {'post': post})
Conclusion
Django’s MTV architecture provides a clean separation of concerns that makes building scalable web applications straightforward:
- Models define your data structure and business logic
- Views handle request processing and orchestration
- Templates render the presentation layer
Key Takeaways
- Models abstract database operations through Django’s ORM
- Views contain business logic and handle requests
- Templates render dynamic HTML with context data
- Use class-based views for reusable, maintainable code
- Keep logic out of templates
- Use select_related and prefetch_related to optimize queries
- Follow Django conventions for consistency and maintainability
Django’s MTV pattern, combined with its rich ecosystem of packages and tools, makes it one of the most productive frameworks for building web applications. Start with these fundamentals, practice building projects, and gradually explore Django’s advanced features like signals, middleware, and custom managers.
Happy coding with Django!
Comments