Django

Django REST APIs (Without DRF)👨‍💻

Django ships with everything you need to build production-grade REST APIs — no third-party framework required. JsonResponse, json.loads(), class-based views, and the authentication system give you full control over request handling, serialization, and access control. While Django REST Framework (DRF) is an excellent tool for large API surfaces, reaching for it on every project adds complexity you may not need.

Building APIs with pure Django teaches you what actually happens at the HTTP layer: how request bodies are parsed, how authentication tokens are validated, and how responses are serialized. This understanding makes you a better API developer whether you eventually use DRF, FastAPI, or stick with vanilla Django.

Key Takeaways

  • 1`JsonResponse` is Django's built-in class for returning JSON — it serializes dicts, sets `Content-Type: application/json`, and handles `datetime`, `Decimal`, and `UUID` via `DjangoJSONEncoder`
  • 2Request bodies arrive as raw bytes in `request.body` — use `json.loads(request.body)` to parse JSON payloads, and always validate the result before trusting it
  • 3Class-based views with `method_dispatch` patterns (or `View` subclass with `get`/`post`/`put`/`delete` methods) keep API endpoints organized without any extra dependencies
  • 4Token authentication in pure Django means writing a middleware or decorator that reads the `Authorization` header, looks up the token, and attaches the user to `request`
  • 5Cursor-based and offset-based pagination are straightforward to implement with QuerySet slicing and `values()` — no pagination library needed
  • 6DRF is worth adopting when you need browsable APIs, automatic schema generation, or complex nested serializers — but understanding the vanilla approach first makes DRF's abstractions transparent

Master django rest apis (without drf)

Take the Django REST API Development course with hands-on lessons and challenges.

Examples

JsonResponse API view with method routing

python

A function-based view that handles GET (list) and POST (create) on the same URL. `require_http_methods` rejects unsupported methods with 405. The serializer function `article_to_dict` controls exactly which fields are exposed.

Parsing and validating request body

python

A reusable parse-then-validate pattern. `parse_json_body` handles Content-Type checking and JSON decoding in one place. The validation function returns a list of error strings, and the view returns 422 with all errors at once instead of failing on the first one.

Class-based API view with View subclass

python

Django's `View` base class dispatches to `get()`, `post()`, `put()`, `delete()` methods automatically and returns 405 for unsupported methods. `csrf_exempt` is needed for non-browser clients that send tokens instead of cookies. The `update_fields` argument avoids overwriting columns you did not intend to change.

Token authentication decorator

python

A decorator that extracts the Bearer token from the Authorization header, hashes it with SHA-256, and looks up the corresponding user. Tokens are stored hashed in the database so a database leak does not expose raw tokens. `secrets.token_urlsafe` generates tokens with sufficient entropy.

Offset-based pagination helper

python

A reusable pagination function that reads `page` and `page_size` from query parameters, clamps them to safe ranges, and returns a consistent envelope with `data` and `pagination` metadata. The `total_pages` calculation uses ceiling division. Callers just pass a queryset and the request.

Common Mistakes

Mistake:

Not checking `request.method` and handling all HTTP methods in a single code path, so a DELETE request accidentally creates a resource

Fix:

Use `require_http_methods` on function views or Django's `View` class which dispatches to `get()`, `post()`, `put()`, `delete()` methods. Always reject methods you do not support — Django's `View` returns 405 automatically for undefined methods.

Mistake:

Returning `200 OK` for every response, including errors, and putting the error status in the JSON body instead

Fix:

Use the `status` parameter on `JsonResponse`: `JsonResponse({"error": "Not found"}, status=404)`. HTTP clients, frontend libraries, and monitoring tools all rely on status codes — a 200 with `{"error": true}` is invisible to error tracking.

Mistake:

Forgetting to exempt API views from CSRF protection when clients send tokens instead of cookies, causing 403 errors on POST/PUT/DELETE

Fix:

Apply `@csrf_exempt` to views that authenticate via `Authorization` headers rather than cookies. CSRF protection guards against browser-based cookie attacks — it is not needed when the authentication mechanism is a bearer token. If you do use session/cookie auth for your API, keep CSRF enabled.

Mistake:

Serializing model instances by calling `model_to_dict()` or `serializers.serialize()` without controlling which fields are included, accidentally exposing password hashes, tokens, or internal flags

Fix:

Write explicit serializer functions like `article_to_dict(article)` that return only the fields the client needs. This is more work upfront but prevents data leaks and gives you control over field names and formatting.

Best Practices

  • Create a consistent response envelope — always return `{"data": ...}` for success and `{"error": ..., "details": [...]}` for errors so clients can parse responses predictably
  • Write a reusable `parse_json_body()` helper that checks `Content-Type`, catches `JSONDecodeError`, and returns a clean error response — this eliminates duplicated parsing logic across views
  • Use `select_related()` and `prefetch_related()` before serialization to avoid N+1 queries — a list endpoint returning 20 articles with authors should make 1-2 queries, not 21
  • Hash API tokens with SHA-256 before storing them in the database — if the database is compromised, attackers cannot reuse the raw tokens
  • Version your API from day one with URL prefixes like `/api/v1/` — it costs nothing upfront and saves you from breaking changes when you need to evolve the API
  • Cap pagination `page_size` to a maximum (e.g., 100) and default to a sane value (e.g., 20) — unbounded page sizes let a single request fetch your entire table

Summary

Django's built-in tools — `JsonResponse`, `json.loads()`, class-based `View`, and the authentication system — are sufficient for building production REST APIs. The key patterns are: explicit serializer functions for safe data exposure, `require_http_methods` or `View` subclasses for method routing, decorator-based token authentication, and reusable pagination helpers. This approach keeps your API surface small and your abstractions transparent. When your API grows to need browsable docs, complex nested serialization, or automatic schema generation, Django REST Framework is a natural next step — but the fundamentals you learn with vanilla Django transfer directly.

Practice Django with hands-on challenges

Learn django rest apis (without drf) hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Django with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.