skip to content

In Django REST Framework, what do APIView and the @api_view decorator add over a plain Django view, and how do they differ?

level: juniorimportance: must knowfreq 66%

answer

  1. same machinery, two spellings
  2. Request in, Response out
  3. policies checked before your code
  4. decorators stack below, methods listed

basics

~20 s

Both give a view DRF's Request and Response, content negotiation, authentication, permission and throttle checks, and API exception handling. APIView is a class with get()/post() methods and policy attributes; @api_view(['GET']) wraps a function into an APIView subclass, with policy decorators.

solid answer

~40 s

`APIView` and `@api_view` run the same machinery: the Django `HttpRequest` is wrapped in a DRF `Request` (`request.data`, `request.query_params`), the view returns a `Response` whose format is negotiated per request, `initial()` runs authentication, permissions and throttles before your code, and `APIException`s become error responses instead of 500s. `APIView` is a class: one method per HTTP verb, policies as class attributes such as `permission_classes`. `@api_view(['GET', 'POST'])` turns a function into a generated `APIView` subclass; unlisted methods get 405 (OPTIONS is always allowed), a bare `@api_view()` means GET only, and policies come from decorators like `@permission_classes([...])`, which must sit below `@api_view`. Both are wrapped in `csrf_exempt`, with `SessionAuthentication` enforcing CSRF itself, and both opt out of Django's `LoginRequiredMiddleware`.

code

python · 30 lines
python
from decimal import Decimal, InvalidOperation

from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response

RATES = {("EUR", "USD"): Decimal("1.0850"), ("USD", "EUR"): Decimal("0.9217")}


@api_view(["GET"])
@permission_classes([AllowAny])  # below @api_view
def convert(request):
    source = request.query_params.get("from", "").upper()
    target = request.query_params.get("to", "").upper()
    try:
        amount = Decimal(request.query_params.get("amount", ""))
    except InvalidOperation:
        return Response(
            {"detail": "amount must be a number"},
            status=status.HTTP_400_BAD_REQUEST,
        )
    rate = RATES.get((source, target))
    if rate is None:
        return Response(
            {"detail": "unsupported currency pair"},
            status=status.HTTP_404_NOT_FOUND,
        )
    result = (amount * rate).quantize(Decimal("0.01"))
    return Response({"from": source, "to": target, "amount": str(amount), "result": str(result)})

go deeper

for a junior

Know both spellings: a class with get()/post() and policy attributes, or a function under @api_view(['GET']) with policy decorators below it, both returning Response.

for a middle

Explain what dispatch() adds: the Request wrapper, initial() running negotiation, authentication, permissions and throttles, exception handling, and how @api_view generates an APIView subclass.

for a senior

Pick the form by reuse needs, keep policies explicit per endpoint where defaults differ, and know the csrf_exempt and LoginRequiredMiddleware interplay when mixing session auth and APIs.

for a principal

Set conventions for when a team writes function views, APIViews or viewsets, so API policies stay consistent and reviewable across many endpoints.

## What a plain Django view lacks A plain Django view receives an `HttpRequest` and returns an `HttpResponse`. For an API that means doing by hand what every endpoint needs: parsing a JSON body, choosing an output format, authenticating the caller from a header, checking permissions and rate limits, and turning errors into JSON. Django REST Framework (DRF) packages that into **`APIView`**, a subclass of Django's `View`, and its function-based twin, the **`@api_view`** decorator. ## What both add 1. **A richer request.** `dispatch()` wraps the `HttpRequest` in a DRF `Request`. `request.data` parses the body with the view's parsers (JSON, form, multipart by default), `request.query_params` exposes the query string, and `request.user`/`request.auth` come from DRF's authentication classes. Unknown attributes are proxied to the underlying `HttpRequest`. 2. **A format-neutral response.** You return `Response(data, status=...)` with plain Python data; the renderer chosen by **content negotiation** (JSON, or the browsable API in a browser, by default) turns it into bytes. 3. **Policy checks before your code.** `initial()` negotiates the format, determines the version, authenticates, then checks permissions and throttles. Only then is the method handler called. 4. **API exception handling.** Raising `NotFound`, `ValidationError`, `PermissionDenied` or Django's `Http404` produces a response with the right status code and a `detail` body; other exceptions still propagate as server errors. 5. **CSRF and login handling for APIs.** `as_view()` wraps the view in `csrf_exempt`; `SessionAuthentication` re-applies CSRF checks only for session-authenticated requests, and token-style authentication needs none. Since DRF 3.16 the view also sets `login_required = False`, so Django's `LoginRequiredMiddleware` (Django 5.1+) leaves it to DRF's permission classes. ## How they differ | Aspect | `APIView` | `@api_view` | |---|---|---| | Shape | class with `get()`, `post()`, ... | function taking `request` | | Allowed methods | methods you define | list passed to the decorator; OPTIONS is always added | | Default methods | — | `@api_view()` with no list means `['GET']` | | Policies | class attributes: `permission_classes`, `throttle_classes`, `parser_classes`, ... | decorators: `@permission_classes([...])`, `@throttle_classes([...])`, ... | | Reuse | inheritance, mixins | none beyond plain functions | Under the hood `@api_view` builds a class named `WrappedAPIView`, assigns your function as the handler for each listed method, copies any policy set by the decorators below it, and returns `WrappedAPIView.as_view()`. The same request lifecycle runs either way. ## Rules that trip people up - **Decorator order.** Policy decorators go **below** `@api_view`, because they attach attributes to the function that `@api_view` then reads. Since DRF 3.17 putting one above raises a `TypeError` saying it must come after (below) `@api_view`. - **Parentheses.** A bare `@api_view` with no call fails an assertion (`@api_view missing list of allowed HTTP methods`); write `@api_view()` or `@api_view(["GET"])`. - **Method list is exact.** Only listed methods (plus OPTIONS) are allowed; even HEAD is refused with 405 unless listed. Methods are conventionally uppercase, and DRF lowercases them for dispatch. - **Return type.** A handler must return an `HttpResponseBase` (usually a `Response`); returning a dict fails an assertion in `finalize_response()`. ## What stays the same as a plain Django view - **Routing** is unchanged: `path("convert/", convert)` for a function view, `path("convert/", ConvertView.as_view())` for a class. - **Django middleware** still wraps the view on the way in and out; DRF's checks run inside the view, after all middleware has processed the request. - **`request.user` has two sources.** Django's `AuthenticationMiddleware` sets a user from the session; inside a DRF view, `request.user` is whatever DRF's own authentication classes decided. When DRF sets it, the `Request.user` setter also writes it to the wrapped `HttpRequest`, so later middleware sees the same user. - **The response is still an `HttpResponse`** subclass: `Response` extends Django's `SimpleTemplateResponse` and is rendered to bytes late, after the renderer is attached. ## Choosing between them - A single small endpoint, like the currency conversion below: `@api_view` is shortest. - Several verbs sharing helpers, or a family of views sharing policies: an `APIView` subclass. - CRUD over a model: neither directly — the generic views and viewsets build on `APIView` and add querysets and serializers.

  • What happens if @permission_classes([IsAuthenticated]) is placed above @api_view instead of below it?
    Decorators apply bottom-up, so `@api_view` would already have built the view class without seeing the permission. Since DRF 3.17 the policy decorator detects that it received an `APIView`-based view and raises a `TypeError` saying it must come after (below) `@api_view`, instead of silently ignoring the policy.
  • Why can a DRF view be csrf_exempt without opening a CSRF hole for browser sessions?
    `APIView.as_view()` exempts the view from Django's CSRF middleware, but `SessionAuthentication` runs Django's CSRF check itself for requests it authenticates from the session cookie. Token or header-based authentication needs no CSRF check, because browsers do not attach those credentials automatically.

saying these in an interview costs you the question

  • @api_view views skip DRF's authentication and permission checks.
  • An @api_view() with no method list accepts every HTTP method.
  • Policy decorators can go above or below @api_view with the same effect.
  • APIView views still need Django's CSRF token on every POST, whatever the authentication.
  • A DRF view can return a plain dict and DRF will wrap it in a Response.