~/TechPurAI
~/tutorials/django-from-scratch/handling-post-and-csrf
intermediate·part 14 of 22·2 min read

Handling POST requests and CSRF protection

Updated Aug 15, 2026Python · Django

{% csrf_token %} has appeared in every form so far without explanation. This part covers what it actually protects against, finishes wiring up PostCreateView/PostUpdateView with a redirect target, and shows what happens when the token is missing.

What CSRF actually is

Cross-Site Request Forgery: a malicious site tricks a logged-in user's browser into submitting a request to your site — the browser automatically attaches that user's session cookie, so without protection, the request looks completely legitimate to your server. A hidden form on an attacker's page that auto-submits to your-blog.com/post/1/delete/ would, without CSRF protection, actually delete the post — because the victim's browser is authenticated, even though the victim never intended to visit your site at all.

What {% csrf_token %} does

django
<form method="post">
  {% csrf_token %}
  ...
</form>

This renders a hidden input containing a token tied to the user's session. Django's CsrfViewMiddleware (already active by default — check MIDDLEWARE in settings.py) rejects any POST request that doesn't include a matching token. An attacker's page, on a different domain, has no way to read that token to forge a valid request — which is exactly the protection.

Seeing it fail

Remove {% csrf_token %} from a form temporarily and submit it:

text
Forbidden (403)
CSRF verification failed. Request aborted.

That's the middleware doing its job. It's worth seeing once deliberately, since the same error shows up for a real, accidental reason later — a form copy-pasted without its {% csrf_token %} line — and it's easier to fix in ten seconds when you recognize it on sight.

Finishing the create/update views

python
# blog/views.py
class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = "blog/post_form.html"
    success_url = reverse_lazy("post_list")

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)


class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = "blog/post_form.html"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def get_success_url(self):
        return reverse_lazy("post_detail", kwargs={"slug": self.object.slug})

success_url on PostCreateView is a fixed redirect target, since there's no existing post to link back to. PostUpdateView overrides get_success_url() instead — a method, not an attribute — because the target depends on self.object, the specific post that was just updated, which isn't known until the view runs.

Common mistake

Setting success_url as a plain string, like success_url = "/posts/", instead of reverse_lazy("post_list"). It works today — until the URL pattern changes and every hardcoded string like it silently points at a dead route.

Next: DeleteView — the last piece of full CRUD, plus the confirmation step that stops a stray click from deleting a post.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous13. ModelForms: CreateView and UpdateViewnext →15. Deleting objects: DeleteView and confirmation templates