Subject

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.

1. What Is a CRUD Application?

A CRUD application is an application that allows users to:

Example: Blog posts

For a blog application:

This pattern is extremely common in Django projects.

2. What You Will Build

In this tutorial, you will build a simple Post Manager application.

It will include:

This is a complete beginner CRUD project.

3. Final CRUD Features

By the end, your application will support:

4. Project Setup

We will assume your project is already created and your app is named blog.

Example structure:

config/
blog/
manage.py

If needed, create the app with:

python manage.py startapp blog

And register it in settings.py:

INSTALLED_APPS = [
    ...
    'blog',
]

5. Step 1: Create the Model

Open blog/models.py.

blog/models.py

from 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.title

Explanation

6. Step 2: Run Migrations

After creating the model, run:

python manage.py makemigrations
python manage.py migrate

This creates the database table.

7. Step 3: Create the ModelForm

Open blog/forms.py.

blog/forms.py

from 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'
            }),
        }

Why use ModelForm?

Because it automatically creates the form based on the model and makes CRUD development easier.

8. Step 4: Create the Views

Now build all CRUD views inside blog/views.py.

blog/views.py

from django.shortcuts import render, get_object_or_404, redirect
from .models import Post
from .forms import PostForm

A. Read All Posts

def 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.

B. Read One Post

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.

C. Create a New 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:

D. Update an Existing Post

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.

E. Delete a Post

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:

9. Full views.py

blog/views.py

from 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})

10. Step 5: Create the URLs

Open blog/urls.py.

blog/urls.py

from 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.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
]

11. Step 6: Create the Templates

Create this folder structure:

blog/
└── templates/
    └── blog/
        ├── base.html
        ├── post_list.html
        ├── post_detail.html
        ├── post_form.html
        └── post_confirm_delete.html

12. Base Template

blog/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.

13. List Template

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.

14. Detail Template

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.

15. Form Template

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.

16. Delete Confirmation Template

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.

17. Step 7: Register the Model in Admin

Open blog/admin.py.

blog/admin.py

from django.contrib import admin
from .models import Post

admin.site.register(Post)

Now you can also manage posts through the Django admin interface.

18. How the CRUD Flow Works

Let us summarize the flow:

Create

Read

Update

Delete

19. Why Reusing the Same Form Template Is Useful

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.

Benefits

20. Improving the Form Page Title

You can make the form template more dynamic.

Update view

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'
    })

Update template

{% 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.

21. Common Beginner Mistakes

Mistake 1: Forgetting instance in update view

Without it, Django creates a new post instead of editing the old one.

Mistake 2: Forgetting {% csrf_token %}

POST forms need CSRF protection.

Mistake 3: Using GET for delete

Deletion should be confirmed with POST, not GET.

Mistake 4: Forgetting redirect()

After create/update/delete, redirecting helps avoid repeated submissions.

Mistake 5: Forgetting URL names

Using named URLs makes templates cleaner and safer.

22. Best Practices

23. Beginner Analogy

Think of a CRUD application like managing books in a library system.

That is exactly what CRUD does for any type of data.

24. Summary

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.

25. Key Takeaways

26. Small Knowledge Check

  1. What does CRUD stand for?
  2. Which view is responsible for showing all posts?
  3. Why do we use instance=post in the update form?
  4. Why is get_object_or_404() useful?
  5. Why should delete actions use POST instead of GET?
  6. Why is it useful to reuse post_form.html for both create and update?

27. Conclusion

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.