Django middleware
Web app development
Redirecting in Django
Routing in Django
Multiple host management

Multiple Host web app (Redirecting/Routing) in Django middleware / view

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A Django application can serve different behavior based on the request host, but the implementation depends on what “different behavior” actually means. Sometimes the right answer is a redirect to a canonical host. Other times the request should stay on the current host and use different URLs, templates, or tenant data internally. Keeping those cases separate prevents a lot of messy middleware.

Decide Between Redirecting and Routing

There are two common host-based behaviors.

Redirecting means the browser should be sent somewhere else, often from an old domain to a new one. Routing means Django should continue handling the request internally, but with different URL configuration or data selection based on the host.

That distinction matters because the code belongs in different places conceptually:

  • redirects are usually global middleware concerns,
  • routing can be middleware, URLConf switching, or tenant resolution,
  • and a view-level host check is only appropriate when the host rule is extremely local.

Before any of that works, the expected hosts must be in ALLOWED_HOSTS.

python
1ALLOWED_HOSTS = [
2    "www.example.com",
3    "old.example.com",
4    ".example.com",
5]

Without that, Django rejects the request before your own logic sees it.

Redirect a Host in Middleware

If one host should always redirect to another, middleware is usually the cleanest home.

python
1from django.http import HttpResponsePermanentRedirect
2
3class CanonicalHostMiddleware:
4    def __init__(self, get_response):
5        self.get_response = get_response
6
7    def __call__(self, request):
8        host = request.get_host().split(":")[0]
9
10        if host == "old.example.com":
11            new_url = f"https://www.example.com{request.get_full_path()}"
12            return HttpResponsePermanentRedirect(new_url)
13
14        return self.get_response(request)

This keeps host migration logic out of individual views and makes the redirect apply consistently across the whole site.

Route Different Hosts to Different URL Configurations

If multiple hosts should stay active but expose different URL maps, middleware can assign request.urlconf.

python
1from django.http import HttpResponseNotFound
2
3class HostRoutingMiddleware:
4    def __init__(self, get_response):
5        self.get_response = get_response
6
7    def __call__(self, request):
8        host = request.get_host().split(":")[0]
9
10        if host == "app.example.com":
11            request.urlconf = "project.urls_app"
12        elif host == "admin.example.com":
13            request.urlconf = "project.urls_admin"
14        else:
15            return HttpResponseNotFound("Unknown host")
16
17        return self.get_response(request)

Then each host can have its own URL file.

python
1# project/urls_app.py
2from django.urls import path
3from .views import app_home
4
5urlpatterns = [
6    path("", app_home),
7]
python
1# project/urls_admin.py
2from django.urls import path
3from .views import admin_home
4
5urlpatterns = [
6    path("", admin_home),
7]

That is usually cleaner than sprinkling if request.get_host() throughout the view layer.

Tenant Routing Is a Different Pattern Again

Sometimes the routes stay the same, but the host identifies a tenant. In that case, middleware often attaches the resolved tenant to the request instead of switching URLConf.

python
1from django.http import HttpResponseNotFound
2from .models import Tenant
3
4class TenantMiddleware:
5    def __init__(self, get_response):
6        self.get_response = get_response
7
8    def __call__(self, request):
9        host = request.get_host().split(":")[0]
10
11        try:
12            request.tenant = Tenant.objects.get(domain=host, is_active=True)
13        except Tenant.DoesNotExist:
14            return HttpResponseNotFound("Tenant not found")
15
16        return self.get_response(request)

Views then use request.tenant normally. This is a better fit when the host changes data selection rather than route shape.

When View-Level Host Checks Are Acceptable

A host check inside a view is fine when only one or two endpoints truly need host-specific behavior and the rule is tightly local.

python
1from django.http import HttpResponseNotFound
2from django.shortcuts import render
3
4def landing_page(request):
5    host = request.get_host().split(":")[0]
6
7    if host == "partners.example.com":
8        return render(request, "partners/landing.html")
9    if host == "www.example.com":
10        return render(request, "public/landing.html")
11
12    return HttpResponseNotFound("Unknown host")

As soon as the same host rule appears in several views, move it back into middleware or a reusable abstraction.

Common Pitfalls

  • Forgetting to include expected domains in ALLOWED_HOSTS.
  • Reading the raw host header instead of using Django’s validated request.get_host().
  • Mixing redirect rules and tenant-routing rules in random views.
  • Accidentally comparing the full host:port string during local development.
  • Using view-level host conditionals for behavior that is really a global routing concern.

Summary

  • Use redirects when the browser should move to a different canonical host.
  • Use middleware routing when the host should change internal URL resolution.
  • Use tenant middleware when the host selects data more than routes.
  • Keep host logic centralized once more than a small number of views depends on it.
  • Always start by configuring ALLOWED_HOSTS correctly.

Course illustration
Course illustration

All Rights Reserved.