Redirects in Django: redirect(), HttpResponseRedirect, and PRG

- Andrés Cruz - ES En español

Video thumbnail

When we develop any web project, Django is no exception: in addition to having named routes to organize our URLs, we need to handle redirects. Redirects are one of those tools we use almost without thinking, but they play a key role in both user experience and technical SEO.

Using redirects properly is what makes the difference between an orderly app and a chaos of broken links.

Redirects in Django are the mechanism to send the user from one URL to another, whether inside our application or to an external domain. In technical terms, a redirect is nothing more than an HTTP response of type 3xx that tells the browser which URL it should go to next. A classic example: when we change the slug of a post without wanting to lose the original URL's ranking, we implement a 301 redirect so that Google transfers the SEO value to the new link.

1. The problem with returning the page directly after a POST

If a POST view —for example, saving or updating a record— responds by directly returning the template with render(), the following happens:

  • The browser remains in a POST state.
  • If the user reloads the page or clicks the "Back" button, the browser will attempt to resend the same POST request, which can duplicate submission, updating, or even deletion of the record.

This behavior is not only a UX issue, but it can also corrupt data if not handled carefully.

2. ✅ The solution: the PRG pattern (Post/Redirect/Get)

To prevent the POST request from being accidentally resubmitted, we use the PRG (Post/Redirect/Get) pattern, a widely used standard in web development:

  • Post: The view receives the POST request and processes the business logic.
  • Redirect: After saving or deleting the record, the view returns a redirect response instead of rendering a template.
  • Get: The browser makes a new GET request to the URL specified by the redirect, usually the list view or the record detail page.

What is an HTTP redirect and why does it matter in Django?

An HTTP redirect is simply an instruction that tells the browser: “don't stay here, go to another URL.”
In Django, this behavior is implemented with specific functions and classes that return an HTTP response of type 3xx.

What redirecting a view means in Django

When a view processes a form or a specific action, it often doesn't have an associated template, because its only responsibility is to execute an operation and send the user somewhere else. In my projects, for example, when a user creates a record, I send them directly to the detail page or to the general list, depending on the corresponding business logic.

Most common status codes: 301, 302, and 307

  • 301 (Permanent): indicates that the URL has changed permanently. It is the ideal code for SEO and when you modify the slug of a page.
  • 302 (Temporary): used when the redirect is temporary, for example, redirecting after submitting a form or while testing a view.
  • 307 (Temporary Strict): a stricter version of 302; it guarantees that the original method (POST or GET) is preserved in the redirect.

When to use 301 vs. 302: quick guide for SEO

When I changed the slug of a post on my blog, I used a 301 redirect to avoid losing traffic or backlinks. Search engines interpret 301 as a permanent transfer of SEO value from the original URL to the new one.

On the other hand, for internal redirects after submitting forms, I always use 302, as they are temporary and should not transfer SEO signals.

Methods for doing redirects in Django

Redirects are the usual flow when a view does not have an associated template. For example, when we process record creation, the expected result is:

  1. If the data is valid: we perform a redirect to the list, or to the edit form for the newly created record.
  2. If validation fails: we redirect back to the creation form showing the corresponding errors.

For the following examples, we start from these routes defined in urls.py:

app_name = "gestion"
urlpatterns = [
   path('', views.index, name="index"),
   path('detail/<int:pk>', views.show, name="show"),
   path('create', views.create, name="create"),
   path('update/<int:pk>', views.update, name="update"),
   path('delete/<int:pk>', views.delete, name="delete"),
]

Django offers several ways to redirect, depending on the context and the level of control needed.

Using redirect() in views (the most direct method)

The redirect() function, available in django.shortcuts, is the most convenient method and the one I use in most of my projects. It accepts a namespaced route name, a relative URL, or an absolute URL:

from django.shortcuts import redirect

return redirect('gestion:index')

A more complete example inside a creation view:

from django.shortcuts import redirect

def create(request):
    form = ProductForm(request.POST or None)
    if form.is_valid():
        product = Product()
        product.title = form.cleaned_data['title']
        # ... rest of the fields ...
        product.save()
        return redirect('gestion:update', pk=product.id)
    return redirect('gestion:create')

The pattern is simple:

  • If everything goes well, I redirect to the edit form for the newly saved record.
  • If something fails in validation, I return to the creation view so the user can correct errors.

HttpResponseRedirect and HttpResponsePermanentRedirect

When you need more explicit control over the HTTP status code, you can directly use the HttpResponseRedirect (which returns a 302) and HttpResponsePermanentRedirect (which returns a 301) classes:

from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect

# Temporary redirect (302)
return HttpResponseRedirect('/productos/')

# Permanent redirect (301) — useful for slug changes and SEO
return HttpResponsePermanentRedirect('/nuevo-producto/')

They are useful when you want to make explicit in code which type of redirect you are applying, without relying on the implicit behavior of redirect().

Redirects with reverse() and dynamic parameters

Django allows building the URL from the route name and its parameters using reverse(). This is especially useful when working with routes that have dynamic parameters like pk or slug:

from django.urls import reverse

return redirect(reverse('gestion:show', kwargs={'pk': 15}))

It is a safer practice than hardcoding URLs, especially if you ever change your route structure in urls.py: with reverse(), you only need to update the route name in a single place.

Redirects to external URLs or other domains

If you need to redirect outside of your application, simply pass the full URL as a string:

return redirect('https://www.google.com')

Be careful with open redirects: if the destination URL originates from user input (e.g., a ?next= parameter), always validate it before redirecting to avoid security vulnerabilities.

redirect() with positional parameters

You can also pass parameters directly to redirect() without needing to use reverse():

return redirect('gestion:update', pk=15)

Django will automatically construct the URL using the route name and the provided parameter value.

Practical use cases from my projects

  • Redirecting after creating or updating a record
    • In my typical workflow, after saving a product, I redirect to detail or list view.
      This improves UX and prevents the user from accidentally reloading the form and duplicating the operation.
  • Form handling: validate, save, and redirect properly
    • If data is invalid, I always redirect back to the original form displaying errors.
      It is a simple touch that avoids user confusion and keeps the application fluid and predictable.
  • Changing a post's slug and redirecting with 301 to prevent traffic loss
    • Once, when updating titles for several articles, the old slugs became obsolete. I implemented a 301 redirect in urls.py to the new routes. The result was clear: I maintained organic traffic without losing search engine rankings.
  • How to avoid infinite loops and common errors
    • A common mistake is redirecting to the same view that triggers the redirect, causing an infinite loop that the browser cuts off with an ERR_TOO_MANY_REDIRECTS error.
    • To prevent it, always verify conditions before returning a redirect response:
    • if request.path != reverse('home'):
          return redirect('home')

Testing and verifying your redirects

How to test redirects using Django's Client()

You can automate tests to ensure your views redirect correctly, without needing to open a browser:

from django.test import Client

client = Client()
response = client.post('/crear/', {'title': 'Nuevo'})
assert response.status_code == 302

Using assertRedirects() in automated tests

Even cleaner and more expressive: the assertRedirects() method checks both status code and target URL in a single line:

self.assertRedirects(response, '/listado/')

Checking redirects with curl or SEO tools

From your terminal, you can inspect HTTP headers for any URL with:

$ curl -I https://tusitio.com/old-url/

This lets you quickly verify whether the returned status code is 301 or 302 and where the Location header points.
You can also review your redirects in Google Search Console or tools like Screaming Frog to crawl your site's full redirect chains.

Common errors and how to solve them

NoReverseMatch and misconfigured namespaces

This error occurs when the URL name doesn't match what is defined in urls.py. Make sure both app_name and path() are configured properly:

app_name = 'gestion'
path('detalle/<int:pk>', views.show, name='show')

Calling redirect('gestion:show', pk=1) with the correct namespace and name will fix the error.

Login/logout redirects not working

If you use generic views like LoginView, define the next parameter or success_url correctly to handle destination after login:

from django.contrib.auth.views import LoginView
from django.urls import reverse_lazy

class CustomLoginView(LoginView):
    success_url = reverse_lazy('dashboard')

Missing parameters or incorrect routes

Make sure to pass all required args or kwargs for the route; if any are missing, Django won't know how to build the URL and will throw a NoReverseMatch.

Frequently asked questions about redirects in Django

  • What is the difference between redirect() and HttpResponseRedirect?
    • redirect() is a high-level shortcut that accepts route names, relative URLs, and absolute URLs, internally deciding the response type. HttpResponseRedirect, on the other hand, always returns a 302 and expects an explicit URL string.
  • Can I redirect with dynamic parameters?
    • Yes, using kwargs directly in redirect(), or combining it with reverse() to safely construct URLs with dynamic arguments.
  • What happens to SEO if I change a URL's slug?
    • If you don't implement a 301 redirect, you will lose organic traffic and backlinks associated with that URL. In my case, setting up the 301 correctly was the difference between maintaining rankings on Google or disappearing.
  • How do I test redirects before deploying to production?
    • Using curl -I <url> in the terminal allows you to view status codes and the Location header. Google Search Console can also help detect misconfigured redirects before they impact rankings.

Conclusion

Redirects in Django are not just a technical tool: they are a key ally for SEO and user experience when used properly.

In my projects, properly applying 301 and 302 codes has helped maintain organic traffic, prevent duplicate submission errors, and create clearer, more predictable application flows.

Planning your routes, validating forms, and testing redirects prior to production will save you many headaches. The PRG pattern is simple, but it makes a world of difference as applications scale.

The next step is to continue with our CRUD and learn how to delete records in Django.

Learn how to use `redirect()` and `HttpResponseRedirect` in Django. Discover how to implement the PRG pattern, perform 301/302 redirects, use `reverse()`, and avoid infinite loops.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.