Custom decorators in Django are one of the most useful tools for writing clean, reusable, and professional view logic. At the beginning of a Django journey, many developers place everything directly inside views: authentication checks, permission checks, logging, request conditions, rate limits, role checks, AJAX verification, ownership validation, or any other repeated rule. This works for a while, especially in small projects. But as the project grows, the same blocks of logic start appearing again and again in many views. This repetition makes the code harder to read, harder to test, and easier to break. That is exactly where custom decorators become powerful. They let you wrap common behavior around views in a reusable and elegant way.

A decorator, in Python, is simply a function that takes another function and returns a new function. In practical terms, that means a decorator can intercept a Django view before it runs, add some logic, and then decide whether the original view should continue. This makes decorators perfect for access control, request validation, logging, timing, or any repeated pre-processing logic that belongs around a view rather than inside it. Django itself already uses decorators heavily, such as @login_required, @permission_required, and @require_POST. Learning to write your own decorators means learning how to create reusable view behavior in exactly the same spirit.

To understand why decorators matter so much, imagine a project with ten views that should only be accessible to staff users. A beginner might write this in every view:

from django.http import HttpResponseForbidden

def dashboard(request):
    if not request.user.is_authenticated:
        return HttpResponseForbidden("You must be logged in.")

    if not request.user.is_staff:
        return HttpResponseForbidden("Staff only.")

    return render(request, "dashboard.html")

Then the same pattern appears in analytics views, admin-like panels, moderation pages, and management tools. This repetition is not only ugly. It is risky. If the logic changes later, you must update every view. If you forget one, the application becomes inconsistent. A custom decorator solves this by moving the repeated logic into one reusable place.

Let us begin with the simplest possible decorator example in pure Python:

def my_decorator(view_func):
    def wrapper(request, *args, **kwargs):
        print("Before the view runs")
        response = view_func(request, *args, **kwargs)
        print("After the view runs")
        return response
    return wrapper

Then you apply it to a Django view:

@my_decorator
def home(request):
    return HttpResponse("Hello from home")

When the view is called, the decorator’s wrapper runs first, then calls the original view, then continues after the view returns a response. This is the core idea behind all Django decorators. Everything else is simply building more useful logic around that pattern.

Now let us build a truly practical custom decorator: a staff_required decorator. This decorator will allow only logged-in staff users to access a view.

from django.http import HttpResponseForbidden
from functools import wraps

def staff_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            return HttpResponseForbidden("You must be logged in.")

        if not request.user.is_staff:
            return HttpResponseForbidden("Staff access only.")

        return view_func(request, *args, **kwargs)
    return wrapper

And then use it like this:

@staff_required
def staff_dashboard(request):
    return render(request, "dashboard/staff_dashboard.html")

This is already a very useful real-world decorator. It centralizes the access logic and keeps the view clean. The @wraps(view_func) part is also important. It preserves metadata from the original view, such as the function name and docstring, which is helpful for debugging, introspection, and compatibility with Django internals.

At this point, it is worth pausing to understand what *args and **kwargs are doing. Django views may receive URL parameters like article_id, slug, or other values passed from the URL configuration. For example:

def article_detail(request, article_id):
    ...

If your decorator wrapper did not accept *args and **kwargs, it could break such views. Including them ensures that whatever arguments Django passes to the original view are preserved.

Now let us build a more realistic decorator for role-based access. Suppose your application uses groups and you want to allow only users in the Editors group:

from django.http import HttpResponseForbidden
from functools import wraps

def group_required(group_name):
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                return HttpResponseForbidden("You must be logged in.")

            if not request.user.groups.filter(name=group_name).exists():
                return HttpResponseForbidden("You do not belong to the required group.")

            return view_func(request, *args, **kwargs)
        return wrapper
    return decorator

This is a parameterized decorator. It is slightly more advanced because the decorator itself takes an argument (group_name). That means we need an outer function that receives the parameter and returns the actual decorator. Then the decorator returns the wrapper. The structure is more nested, but the idea remains the same.

Usage:

@group_required("Editors")
def editor_panel(request):
    return render(request, "editor/panel.html")

This is a very nice example because it shows how decorators can become flexible and reusable without hardcoding everything.

A very common use case for custom decorators in Django is request-method validation. Django already provides decorators like @require_POST, but writing a custom version is educational and helps you understand how decorators work. For example:

from django.http import HttpResponseNotAllowed
from functools import wraps

def require_post_only(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if request.method != "POST":
            return HttpResponseNotAllowed(["POST"])
        return view_func(request, *args, **kwargs)
    return wrapper

Usage:

@require_post_only
def submit_comment(request):
    return HttpResponse("Comment submitted")

This kind of decorator is useful because it clearly expresses the intended request rule at the top of the view instead of burying it inside the view body.

Another strong use case is AJAX or API-style request restrictions. Suppose you want a view to only accept requests coming from JavaScript clients that send a special header:

from django.http import JsonResponse
from functools import wraps

def ajax_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if request.headers.get("X-Requested-With") != "XMLHttpRequest":
            return JsonResponse({"error": "AJAX requests only."}, status=400)
        return view_func(request, *args, **kwargs)
    return wrapper

Usage:

@ajax_required
def load_notifications(request):
    return JsonResponse({"count": 5})

This kind of decorator can help protect endpoints intended for specific usage patterns.

Now let us move toward deeper practical design. One of the best use cases for decorators is logging repeated events. For example, suppose you want to log whenever a view is accessed:

import logging
from functools import wraps

logger = logging.getLogger(__name__)

def log_view_access(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        logger.info(
            f"User {request.user} accessed {view_func.__name__}"
        )
        return view_func(request, *args, **kwargs)
    return wrapper

Usage:

@log_view_access
def reports(request):
    return render(request, "reports.html")

This is elegant because you can apply it only where needed, and the view itself stays focused on business logic rather than repeated logging code.

You can also use decorators to measure execution time:

import time
import logging
from functools import wraps

logger = logging.getLogger(__name__)

def measure_view_time(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        start = time.time()
        response = view_func(request, *args, **kwargs)
        duration = time.time() - start
        logger.info(f"{view_func.__name__} took {duration:.4f} seconds")
        return response
    return wrapper

This is a very nice example because it shows that decorators are not only for access control. They are useful whenever you want reusable behavior that wraps around a view.

Another common real-world use case is ownership checking. Suppose a user should only be able to edit their own article. A decorator can centralize that logic. Let us say your URL contains article_id:

from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
from functools import wraps
from .models import Article

def author_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        article_id = kwargs.get("article_id")
        article = get_object_or_404(Article, id=article_id)

        if article.author != request.user:
            return HttpResponseForbidden("You are not the author of this article.")

        return view_func(request, *args, **kwargs)
    return wrapper

Usage:

@author_required
def edit_article(request, article_id):
    ...

This is powerful because it extracts ownership logic from the view. However, it also teaches an important design lesson: decorators should remain readable and not become too magical. If a decorator starts doing too much hidden work, future maintenance may become harder. A decorator like author_required is fine when the rule is reused consistently, but if the logic becomes highly specific and complex, it may be better placed in the view or a service layer.

Another useful pattern is combining decorators. For example:

@login_required
@permission_required("blog.change_article", raise_exception=True)
@author_required
def edit_article(request, article_id):
    ...

Python applies decorators from the bottom upward, so the order matters. This is a very important concept. In this example:

  1. author_required wraps the view first,
  2. then permission_required,
  3. then login_required.

Decorator order can change behavior significantly. For example, a decorator that assumes an authenticated user should usually come after login_required or include its own authentication check. If the order is wrong, your decorator may fail unexpectedly when it tries to use request.user assumptions on anonymous users.

Now let us examine parameterized decorators more deeply, because they are extremely useful in Django. Suppose you want to create a decorator that allows only users with a minimum account age:

from django.http import HttpResponseForbidden
from functools import wraps
from datetime import timedelta
from django.utils.timezone import now

def minimum_account_age_required(days):
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                return HttpResponseForbidden("You must be logged in.")

            if request.user.date_joined > now() - timedelta(days=days):
                return HttpResponseForbidden(
                    f"Your account must be at least {days} days old."
                )

            return view_func(request, *args, **kwargs)
        return wrapper
    return decorator

Usage:

@minimum_account_age_required(30)
def post_in_forum(request):
    return HttpResponse("Forum post page")

This is a nice example because it shows how decorators can accept configurable rules while remaining reusable.

Custom decorators also work very well with JSON APIs. Suppose you are building endpoints that should only be accessible to authenticated users with a custom token in the headers:

from django.http import JsonResponse
from functools import wraps

def api_token_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        token = request.headers.get("X-API-TOKEN")

        if token != "expected-secret-token":
            return JsonResponse({"error": "Invalid API token."}, status=403)

        return view_func(request, *args, **kwargs)
    return wrapper

Usage:

@api_token_required
def protected_api(request):
    return JsonResponse({"message": "Success"})

In a real application, of course, the token should not be hardcoded, but this example shows the structure clearly.

One of the most important best practices with decorators is to keep them focused. A decorator should usually do one thing well. A “staff_required” decorator should enforce staff access. A “measure_view_time” decorator should measure time. If you build giant decorators that check authentication, permissions, rate limits, logging, ownership, feature flags, and request headers all at once, the code becomes hard to understand and hard to debug. Small, single-purpose decorators are usually much better.

Another important best practice is using meaningful names. A decorator should communicate clearly what it does. Names like:

  • staff_required
  • group_required
  • ajax_required
  • author_required
  • measure_view_time

are much better than vague names like:

  • check_user
  • protect_view
  • validate_request

Good names make decorators read almost like plain English above the view.

It is also useful to understand the difference between decorators and middleware. Both can wrap view execution, but they operate at different scopes. Middleware applies globally to many or all requests in the project. A decorator applies only to the views where you place it. If the logic belongs to every request, middleware may be a better fit. If the logic belongs to selected views, a decorator is often cleaner. For example:

  • global request logging: middleware
  • protect only staff views: decorator
  • add a request ID to every request: middleware
  • require a user to own an article: decorator

This distinction helps you choose the right abstraction.

Decorators also differ from mixins in class-based views. In class-based views, mixins are often a more natural way to share behavior because they fit the class structure. For example, LoginRequiredMixin and PermissionRequiredMixin are mixin-based rather than decorator-based. However, Django also allows you to apply function decorators to class-based views using method_decorator. For example:

from django.utils.decorators import method_decorator
from django.views.generic import TemplateView

@method_decorator(staff_required, name="dispatch")
class StaffDashboardView(TemplateView):
    template_name = "dashboard/staff_dashboard.html"

This is very useful because it brings your custom function decorator into the class-based view world. The dispatch method is the right target because it sits at the entry point of the request handling process for class-based views.

You can also apply decorators directly to class methods:

from django.utils.decorators import method_decorator
from django.views import View

class MyView(View):
    @method_decorator(staff_required)
    def get(self, request, *args, **kwargs):
        return HttpResponse("Hello")

But decorating dispatch is usually more general when you want the rule to apply to all HTTP methods.

Testing custom decorators is very important. Because decorators affect view behavior, they should be verified just like views themselves. For example, suppose you have staff_required. You should test:

  • anonymous user is denied,
  • normal authenticated user is denied,
  • staff user is allowed.

A simple test pattern might look like this:

from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse

class StaffDecoratorTests(TestCase):
    def setUp(self):
        self.normal_user = User.objects.create_user(
            username="user1",
            password="test12345"
        )
        self.staff_user = User.objects.create_user(
            username="staff1",
            password="test12345",
            is_staff=True
        )

    def test_normal_user_cannot_access_staff_view(self):
        self.client.login(username="user1", password="test12345")
        response = self.client.get(reverse("staff_dashboard"))
        self.assertEqual(response.status_code, 403)

    def test_staff_user_can_access_staff_view(self):
        self.client.login(username="staff1", password="test12345")
        response = self.client.get(reverse("staff_dashboard"))
        self.assertEqual(response.status_code, 200)

This is a good habit because decorators often enforce important security or business rules.

Another subtle but important point is that decorators should not hide too much application logic. For example, a decorator that silently loads objects, changes state, catches exceptions, and redirects in complex ways may become confusing for future developers. A decorator should usually be transparent in purpose. If you read @staff_required, the behavior is clear. If you read @prepare_full_article_context_and_validate_multistep_workflow, the code may be trying to do too much. Clarity matters.

You should also be careful with database-heavy decorators. If a decorator performs expensive queries on every request, it can hurt performance. For example, an ownership decorator that loads several related models on every request may be acceptable for selected views, but if used very widely, it should be designed carefully. Decorators are not automatically cheap simply because they look small.

Let us build one more strong example: a decorator that only allows access during working hours:

from django.http import HttpResponseForbidden
from functools import wraps
from django.utils.timezone import localtime

def office_hours_required(start_hour=9, end_hour=17):
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            current_hour = localtime().hour

            if current_hour < start_hour or current_hour >= end_hour:
                return HttpResponseForbidden("This view is only available during office hours.")

            return view_func(request, *args, **kwargs)
        return wrapper
    return decorator

Usage:

@office_hours_required(8, 18)
def internal_support_view(request):
    return HttpResponse("Support panel")

This is another nice demonstration of how custom decorators let you turn repeated business rules into elegant reusable tools.

A useful summary pattern for writing decorators is:

  1. Decide what repeated rule belongs around a view.
  2. Write a wrapper that takes request, *args, **kwargs.
  3. Use @wraps(view_func).
  4. Return an appropriate response if the rule fails.
  5. Call the original view if the rule passes.
  6. Keep the decorator focused and well named.
  7. Test it.

In conclusion, custom decorators in Django are a powerful way to keep views clean, reusable, and expressive. They allow you to take repeated logic such as access control, request validation, logging, timing, role checks, and ownership rules out of individual views and place them into reusable wrappers. Django itself already uses decorators extensively, and learning to write your own helps you build applications that feel more organized and professional. The most important lessons are to understand the wrapper pattern, preserve metadata with @wraps, support *args and **kwargs, use parameterized decorators when needed, keep decorators focused, pay attention to order when combining them, and remember that decorators are best for view-specific repeated behavior rather than global request processing. Once you become comfortable with custom decorators, you start writing Django views that are much cleaner, easier to maintain, and much more elegant.

What you learned in this tutorial

In this tutorial, you learned what custom decorators are, how they wrap Django views, why they help remove repeated logic from views, how to write simple and parameterized decorators, how to use @wraps, how to handle request arguments safely, how to create decorators for staff access, group checks, request method validation, AJAX checks, logging, timing, and ownership rules, how to use decorators with class-based views through method_decorator, and why decorator order, testing, and clarity matter.