Build a complete CRUD application in Django by combining models, views, URLs, templates, and ModelForms.
At this stage, you already know the most important Django building blocks: models, views, templates, URLs, forms, and ModelForms. Now it is time to combine them into one of the most useful patterns in web development: CRUD.
CRUD stands for:
These four operations are the foundation of many real applications such as blogs, task managers, student systems, product catalogs, and admin dashboards.
In this tutorial, you will build a complete CRUD application in Django from scratch and understand how all its parts work together.
A CRUD application is an application that allows users to:
For a blog application:
This pattern is extremely common in Django projects.
In this tutorial, you will build a simple Post Manager application.
It will include:
This is a complete beginner CRUD project.
By the end, your application will support:
We will assume your project is already created and your app is named blog.
Example structure:
config/
blog/
manage.pyIf needed, create the app with:
python manage.py startapp blogAnd register it in settings.py:
INSTALLED_APPS = [
...
'blog',
]Open blog/models.py.
blog/models.pyfrom django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
is_published = models.BooleanField(default=True)
def __str__(self):
return self.titletitle stores the post titlecontent stores the post textauthor stores the author namecreated_at stores creation date automaticallyis_published allows publication statusAfter creating the model, run:
python manage.py makemigrations
python manage.py migrateThis creates the database table.
Open blog/forms.py.
blog/forms.pyfrom django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'author', 'is_published']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter post title'
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'placeholder': 'Write your post content',
'rows': 6
}),
'author': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter author name'
}),
}Because it automatically creates the form based on the model and makes CRUD development easier.
Now build all CRUD views inside blog/views.py.
blog/views.pyfrom django.shortcuts import render, get_object_or_404, redirect
from .models import Post
from .forms import PostFormdef post_list(request):
posts = Post.objects.all().order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})This view retrieves all posts and sends them to the list template.
def post_detail(request, post_id):
post = get_object_or_404(Post, id=post_id)
return render(request, 'blog/post_detail.html', {'post': post})This view displays one specific post.
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
form.save()
return redirect('blog:post_list')
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})This view:
def post_update(request, post_id):
post = get_object_or_404(Post, id=post_id)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('blog:post_detail', post_id=post.id)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {'form': form})This view uses instance=post so Django edits the existing object instead of creating a new one.
def post_delete(request, post_id):
post = get_object_or_404(Post, id=post_id)
if request.method == 'POST':
post.delete()
return redirect('blog:post_list')
return render(request, 'blog/post_confirm_delete.html', {'post': post})This view:
views.pyblog/views.pyfrom django.shortcuts import render, get_object_or_404, redirect
from .models import Post
from .forms import PostForm
def post_list(request):
posts = Post.objects.all().order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, post_id):
post = get_object_or_404(Post, id=post_id)
return render(request, 'blog/post_detail.html', {'post': post})
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
form.save()
return redirect('blog:post_list')
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
def post_update(request, post_id):
post = get_object_or_404(Post, id=post_id)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('blog:post_detail', post_id=post.id)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {'form': form})
def post_delete(request, post_id):
post = get_object_or_404(Post, id=post_id)
if request.method == 'POST':
post.delete()
return redirect('blog:post_list')
return render(request, 'blog/post_confirm_delete.html', {'post': post})Open blog/urls.py.
blog/urls.pyfrom django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<int:post_id>/', views.post_detail, name='post_detail'),
path('create/', views.post_create, name='post_create'),
path('update/<int:post_id>/', views.post_update, name='post_update'),
path('delete/<int:post_id>/', views.post_delete, name='post_delete'),
]Now connect the app URLs in config/urls.py.
config/urls.pyfrom django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')),
]Create this folder structure:
blog/
└── templates/
└── blog/
├── base.html
├── post_list.html
├── post_detail.html
├── post_form.html
└── post_confirm_delete.htmlblog/templates/blog/base.html<!DOCTYPE html>
<html>
<head>
<title>Django CRUD App</title>
</head>
<body>
<h1><a href="{% url 'blog:post_list' %}">Django CRUD App</a></h1>
<hr>
{% block content %}
{% endblock %}
</body>
</html>
This template is shared by all pages.
blog/templates/blog/post_list.html{% extends 'blog/base.html' %}
{% block content %}
<h2>All Posts</h2>
<p>
<a href="{% url 'blog:post_create' %}">➕ Create New Post</a>
</p>
{% if posts %}
<ul>
{% for post in posts %}
<li>
<a href="{% url 'blog:post_detail' post.id %}">
{{ post.title }}
</a>
by {{ post.author }}
</li>
{% endfor %}
</ul>
{% else %}
<p>No posts available.</p>
{% endif %}
{% endblock %}This page displays all posts.
blog/templates/blog/post_detail.html{% extends 'blog/base.html' %}
{% block content %}
<h2>{{ post.title }}</h2>
<p><strong>Author:</strong> {{ post.author }}</p>
<p><strong>Created:</strong> {{ post.created_at }}</p>
<p><strong>Published:</strong> {{ post.is_published }}</p>
<p>{{ post.content }}</p>
<p>
<a href="{% url 'blog:post_update' post.id %}">✏️ Edit</a>
|
<a href="{% url 'blog:post_delete' post.id %}">🗑️ Delete</a>
</p>
<p>
<a href="{% url 'blog:post_list' %}">← Back to all posts</a>
</p>
{% endblock %}This page displays one post in detail.
blog/templates/blog/post_form.html{% extends 'blog/base.html' %}
{% block content %}
<h2>Post Form</h2>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
<p>
<a href="{% url 'blog:post_list' %}">← Back to all posts</a>
</p>
{% endblock %}
This template is reused for both create and update.
blog/templates/blog/post_confirm_delete.html{% extends 'blog/base.html' %}
{% block content %}
<h2>Delete Post</h2>
<p>Are you sure you want to delete "{{ post.title }}"?</p>
<form method="POST">
{% csrf_token %}
<button type="submit">Yes, Delete</button>
</form>
<p>
<a href="{% url 'blog:post_detail' post.id %}">Cancel</a>
</p>
{% endblock %}This prevents accidental deletion.
Open blog/admin.py.
blog/admin.pyfrom django.contrib import admin
from .models import Post
admin.site.register(Post)Now you can also manage posts through the Django admin interface.
Let us summarize the flow:
/create///update/1//delete/1/Notice that both create and update views use:
return render(request, 'blog/post_form.html', {'form': form})
This is a very common and smart Django practice.
You can make the form template more dynamic.
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
form.save()
return redirect('blog:post_list')
else:
form = PostForm()
return render(request, 'blog/post_form.html', {
'form': form,
'page_title': 'Create Post'
}){% extends 'blog/base.html' %}
{% block content %}
<h2>{{ page_title }}</h2>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{% endblock %}Do the same for update:
'page_title': 'Update Post'This makes the interface clearer.
instance in update viewWithout it, Django creates a new post instead of editing the old one.
{% csrf_token %}POST forms need CSRF protection.
Deletion should be confirmed with POST, not GET.
redirect()After create/update/delete, redirecting helps avoid repeated submissions.
Using named URLs makes templates cleaner and safer.
get_object_or_404() for detail, update, and deleteModelForm for model-based formsThink of a CRUD application like managing books in a library system.
That is exactly what CRUD does for any type of data.
In this tutorial, you built a complete CRUD application in Django using a model, ModelForm, views, URLs, and templates. You created pages to list, display, add, update, and delete posts. This is one of the most important milestones in Django because it shows how all the main concepts work together in a real application.
ModelForm makes create and update easierinstance is required for editing existing recordsget_object_or_404() is useful for safe retrievalinstance=post in the update form?get_object_or_404() useful?post_form.html for both create and update?A CRUD application is one of the best ways to practice Django because it forces you to combine everything you have learned so far. Once you can build CRUD applications confidently, you are ready for more realistic features like authentication, messages, file uploads, search, and pagination.