Django / Request and data foundations / Lesson 3
Views, methods, and truthful HTTP responses
Write small request handlers whose methods, validation, content types, and status codes express the actual operation.
On this page
Lesson goals
- A view owns a request boundary
- Restrict the declared method explicitly
- Keep errors observable without leaking internals
A view owns a request boundary
A function-based view accepts a request and returns a response or raises an appropriate exception handled by the framework. Keep HTTP concerns such as method, query parsing, and response formatting visible while moving reusable domain calculations into ordinary functions. A view should not silently perform a write because a page was fetched. Safe methods such as GET should be free of requested state changes; creation and mutation belong to deliberate unsafe-method workflows with the relevant protection. HTTP method semantics and authorization are separate: using POST does not prove a caller may perform a change. Document both parts of the contract.
Read each input from the intended source
request.GET is a query-data mapping, while request.POST represents supported form-encoded input rather than every possible request body. A JSON API must decode and validate its body under an explicit size, media-type, and error policy. QueryDict can contain multiple values for one key, so decide whether duplicates are valid instead of accidentally accepting whichever value a convenience read returns. Convert numeric text deliberately and reject malformed or out-of-range values with useful feedback. Never pass an arbitrary ordering field, redirect target, or model attribute directly from request text into a privileged operation without a narrow allowlist.
Restrict the declared method explicitly
The first example applies require_GET to a simple status view and invokes it with synthetic GET and POST requests. The rejected method receives 405 rather than accidentally executing the read handler. This decorator does not add authentication, rate limits, or CSRF behavior; it expresses the method boundary only. The local direct-call fixture makes that limited result reproducible without a server. A production health or status endpoint also needs a deliberate information policy so it does not expose secrets or expensive diagnostics to every caller. A small success response should correspond to the limited condition it actually tested.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
from django.http import HttpResponse
from django.test import RequestFactory
from django.views.decorators.http import require_GET
@require_GET
def status(request):
return HttpResponse("ready", content_type="text/plain")
factory = RequestFactory()
print(status(factory.get("/status/")).status_code)
print(status(factory.post("/status/")).status_code)
200
405
The decorator admits GET and rejects POST under the same direct-call fixture. No network or health dependency is contacted.
Return structured data under a clear schema
The second example returns a JsonResponse containing an explicit public projection. A dictionary is the normal safe top-level shape for this helper; other shapes require an intentional safe setting and API contract. Do not serialize an entire model or request simply because a JSON helper accepts data. Keep types stable, define missing values, and use a meaningful status. A 400 input rejection, 403 permission failure, 404 missing resource, and 500 unexpected server failure communicate different outcomes. A plain JSON view is not a full REST framework: pagination, authentication, schema evolution, and error conventions still need design.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
import json
from django.http import JsonResponse
response = JsonResponse({"course": "Django", "seats": 4}, status=200)
payload = json.loads(response.content)
print(response.status_code)
print(payload["course"], payload["seats"])
200
Django 4
The response carries only the selected public fields, and the numeric seat value remains a JSON number.
Keep errors observable without leaking internals
Catch only failures the view can classify meaningfully, and let unexpected exceptions reach the applicationโs reviewed error-reporting boundary. Returning a successful empty list for every database exception can turn an outage into a false business answer. Log safe context such as an operation identifier and error class, not complete passwords, tokens, or submitted records. Tests should inspect status, content type, and the exact approved fields independently. Include unsupported methods, duplicate inputs, empty data, and failure recovery. Direct RequestFactory checks are useful for local logic but do not establish reverse-proxy, browser, or end-to-end middleware behavior.
Common errors and fixes
JSON input is absent from request.POST
- Why it happens
- The body is JSON rather than form-encoded data.
- How to fix it
- Decode and validate the declared body format explicitly.
- Prevent it next time
- Test each supported media type separately.
An outage looks like an empty successful catalog
- Why it happens
- A broad exception handler substitutes normal-looking data.
- How to fix it
- Report classified failures truthfully and preserve unexpected errors.
- Prevent it next time
- Test failure versus genuinely empty success.
A read URL changes records
- Why it happens
- GET was used to trigger mutation.
- How to fix it
- Move the change to an authorized protected write workflow.
- Prevent it next time
- Review method semantics with route tests.
Check your understanding
1. Why is returning status 200 with {"error": "denied"} a weak permission contract?
Show worked solution
Clients, caches, and monitoring can interpret the status as successful even though the body says otherwise. Use the documented permission status and safe error shape, and assert both in tests. Keep internal diagnostics restricted rather than exposing every reason or record detail publicly.
2. Design a numeric page-size parser for a bounded endpoint.
Show worked solution
Accept a declared text field, convert it under a narrow error boundary, and require an allowed range such as one through fifty. Decide how duplicate query keys and omitted values behave. Return a clear rejection for malformed data instead of allowing an unbounded database or memory request.
Key takeaways
- Methods, input formats, and permissions are separate contracts.
- Status codes must match real outcomes.
- Serialize only intentional public fields.