Pustakam Library

Free Programming learning guide

Build a REST API with Django REST Framework

Build a REST API with Django REST Framework — a free intermediate-level guide covering how to build a rest api with django. Learn with clear...

82 min read10 chaptersintermediate

What you will learn

  1. Introduction to Django REST Framework
  2. Serializers for Data Conversion
  3. Views and ViewSets
  4. Routing and URL Configurations
  5. Authentication and Permissions
  6. Advanced Serialization and Relationships
  7. Filtering, Searching, and Pagination
  8. API Testing and Debugging
  9. Performance Optimization and Caching
  10. Production Deployment and CORS

1. Introduction to Django REST Framework

The Shift from Server-Rendered Templates to API Endpoints Imagine your Django application has just been acquired by a rapidly expanding logistics company. The web frontend built with Django templates is still useful for the administrative team, but the business now requires a dedicated mobile app for delivery drivers, a real-time tracking dashboard built in React for dispatchers, and an integration with a third-party warehouse management system. You could attempt to shoehorn all these requirements into traditional Django views, manually parsing request.body for JSON and returning HttpResponse objects with hardcoded application/json content types. But doing so means rebuilding the infrastructure for content negotiation, request parsing, and response formatting from scratch. This is where Django REST Framework (DRF) steps in. DRF is a powerful, flexible toolkit for building Web APIs in Django. It abstracts away the tedious boilerplate associated with HTTP APIs, providing standardized patterns for parsing requests, negotiating content types, and formatting responses. While standard Django is designed around the request-response cycle for HTML, DRF extends this cycle to treat JSON (and other media types) as first-class citizens. Installing and Configuring DRF Since you are already familiar with Django, we will move briskly through the setup. DRF is simply a Python package that layers on top of your existing Django project. First, activate your virtual environment and install the package using pip: Next, you need to add DRF to your Django project's INSTALLEDAPPS in settings.py. The convention is to place it directly after the default Django apps and before your local applications: Basic Configuration DRF works out of the box with sensible defaults, but you can explicitly configure its behavior by adding a RESTFRAMEWORK dictionary to your settings.py. For now, we will set up a basic configuration that defines the default authentication and permission classes. (Note: Authentication and permissions are covered in depth in Chapter 5, but defining the defaults now prevents unexpected behavior as we build our first endpoints). With AllowAny set, our API endpoints will be publicly accessible by default, which is exactly what we want while setting up our initial environment. The DRF Request-Response Lifecycle To build effective APIs, you must understand how DRF extends Django’s standard lifecycle. Standard Django uses HttpRequest and HttpResponse objects. DRF introduces two specialized objects: Request and Response. The Request Object DRF's Request object extends Django's standard HttpRequest, adding critical functionality for API development. The most important enhancement is how it handles data parsing. In standard Django, accessing incoming data requires checking the HTTP method and manually parsing the body: DRF abstracts this into a single, unified property: request.data. request.data automatically parses the incoming request based on the Content-Type header. Whether the client sends JSON, form data, or multipart file uploads, request.data resolves …

2. Serializers for Data Conversion

The Bridge Between Models and JSON Your database speaks in rows, foreign keys, and Python objects. Your API clients speak in JSON. Left unmanaged, the translation between these two worlds devolves into a tangled mess of json.dumps(), manual dictionary construction, and fragile KeyError exceptions. In the Introduction to Django REST Framework, we saw how DRF intercepts incoming requests and parses them into request.data using its Unified Data Parsing. But once DRF hands you that neatly parsed dictionary, what happens next? How do you validate that dictionary against your business logic, and how do you translate a Django ORM model back into a safe JSON payload via Content Negotiation and Response Rendering? The answer is the serializer. Serializers act as the strict border control of your API. They ensure that no invalid data breaches your database, and no sensitive or improperly formatted data leaks out to the client. Defining ModelSerializers At its core, a serializer is a class that defines how complex data types—like Django ORM models—are converted to native Python datatypes, which are then easily rendered into JSON. While you can write a serializer from scratch by explicitly declaring every field, doing so for a database model is highly repetitive. DRF provides the ModelSerializer class to shortcut this process. A ModelSerializer automatically generates a set of fields from your Django model, along with their appropriate validation rules (like maxlength or null=True), and implements standard .create() and .update() methods. Consider a standard Django model for a SaaS application's billing system: To expose the Plan model, we define a ModelSerializer: When DRF processes a request using this serializer, it leverages the Request Intercept pipeline. If a client sends a POST request with a JSON payload, DRF parses it into request.data. You then pass that data into the serializer: By default, ModelSerializer handles the heavy lifting. However, relying solely on model-level constraints (like maxlength or EmailField) is rarely sufficient for production applications. You need custom validation. Implementing Custom Validation Logic DRF’s validation system runs in a specific, hierarchical order. Understanding this flow is critical for intermediate developers who need to enforce complex business rules. When you call serializer.isvalid(), DRF executes validation in this sequence: 1. Field-level validation: Custom validate<fieldname methods. 2. Object-level validation: The validate() method. 3. Validator classes: Any explicit validators assigned to the field (e.g., UniqueValidator). Field-Level Validation Field-level validation is perfect for sanitizing or checking a single field in isolation. You implement it by defining a method named validate<fieldname on your serializer. In our Subscription model, we want to ensure that the customeremail belongs to a corporate domain, blocking free email providers like Gmail or Yahoo. Notice that if validation passes, we return value. DRF expects the validated value to …

3. Views and ViewSets

The Problem with Function-Based Views at Scale In the previous chapter, we used The @apiview Decorator to handle incoming HTTP requests and return DRF responses. For a simple endpoint or two, function-based views are perfectly adequate. But imagine building an e-commerce API. You need endpoints to create products, list them, retrieve individual items, update stock counts, and delete discontinued items. If you write a function-based view for each action, you will quickly find yourself repeating the same boilerplate: querying the database, initializing a ModelSerializer, validating request.data, saving, and returning a Response. This repetition isn’t just tedious; it’s a maintenance liability. If the logic for how you fetch a product changes, you have to update it across multiple separate functions. Class-based views (CBVs) and ViewSets solve this by allowing you to compose behavior rather than copy-paste it. DRF takes Django’s generic CBVs and supercharges them for REST, providing a structured, object-oriented way to define API endpoints. From Function-Based Views to APIView The foundation of all class-based views in DRF is APIView. It bridges the gap between Django’s View class and DRF’s request/response cycle, ensuring that Unified Data Parsing, Content Negotiation, and Response Rendering happen automatically. Instead of branching on request.method inside a single function, APIView lets you define methods (get, post, put, delete) directly on the class: While APIView organizes our code better than a function-based view, we are still manually writing the logic to fetch all objects, serialize them, and handle validation. To eliminate this boilerplate, we need to use Generic Views and Mixins. Building CRUD Endpoints with Mixins and Generic Views DRF provides a powerful module of Mixins and Generic Views that encapsulate standard CRUD operations. Instead of writing the logic yourself, you simply inherit the behavior and provide the configuration (like which model and serializer to use). The Building Blocks: Mixins Mixins are modular classes that provide a single, specific piece of REST behavior. DRF provides five core mixins: ListModelMixin: Provides a .list() method to retrieve a queryset and serialize it into a list. CreateModelMixin: Provides a .create() method to validate request.data, instantiate a serializer, and save a new object. RetrieveModelMixin: Provides a .retrieve() method to fetch a single object by primary key and return its serialized data. UpdateModelMixin: Provides an .update() method (and .partialupdate()) to modify an existing object. DestroyModelMixin: Provides a .destroy() method to delete a single object. Combining Mixins with Generic Views Mixins cannot stand alone; they must be attached to a Generic View. Generic Views handle the underlying HTTP request routing and provide essential attributes like queryset and serializerclass. For example, if you only want an endpoint to list and create tasks, you can combine GenericAPIView with ListModelMixin and CreateModelMixin: Notice that we still …

4. Routing and URL Configurations

The Anatomy of a URLconf A client makes a request to https://api.example.com/v1/users/42/. Before Django REST Framework can leverage its Content Negotiation to determine the output format, or parse the incoming payload via Unified Data Parsing, Django’s URL resolver must map that URL path to a specific Python callable. This mapping is the sole responsibility of the URL configuration (URLconf). In a standard Django application, you define these mappings using tuples of URL patterns and their corresponding views. DRF seamlessly integrates with this existing system. Whether you are using the @apiview Decorator for function-based views or class-based views that return DRF’s Response objects, the URL configuration process remains fundamentally the same. Routing to Function-Based Views Function-based views (FBVs) wrapped with the @apiview decorator are mapped directly in the URLconf. Because DRF’s @apiview transforms a standard function into a view capable of handling DRF Request objects, you simply point the URL pattern directly at the function. Routing to Class-Based Views Class-based views (CBVs) and DRF's generic views require an extra step. Because they are classes rather than functions, Django needs to instantiate them and trigger the correct HTTP method handler (e.g., get(), post()). DRF provides the asview() method for exactly this purpose. A critical best practice here is the inclusion of the name argument. Naming your routes allows you to decouple your API structure from your application logic. If you later need to change articles/ to blog/posts/, your frontend or internal services relying on reverse('article-list') will automatically resolve to the new URL without breaking. Organizing Multiple URLconfs As your API grows, a single urls.py file becomes unmanageable. Django allows you to include other URLconfs, enabling a modular architecture. This is particularly useful when versioning your API or separating domains. Automating Routes with Routers In the previous chapter, we explored how ViewSets group related logic into a single class, eliminating the need to write separate views for list, create, retrieve, update, and destroy operations. However, if you wire up a ViewSet manually, you end up writing repetitive URL patterns for each of these actions. DRF solves this by introducing Routers. A router automatically maps URL patterns to the corresponding methods on your ViewSet, generating the standard RESTful endpoints for you. The SimpleRouter The SimpleRouter provides the most straightforward routing mechanism. It generates two URL patterns per ViewSet: one for the list/collection view and one for the detail/item view. By registering ArticleViewSet with the prefix articles, the SimpleRouter automatically generates the following mappings: - GET /api/articles/ - list() - POST /api/articles/ - create() - GET /api/articles/{pk}/ - retrieve() - PUT /api/articles/{pk}/ - update() - PATCH /api/articles/{pk}/ - partialupdate() - DELETE /api/articles/{pk}/ - destroy() The DefaultRouter The DefaultRouter extends the SimpleRouter by adding an API …

5. Authentication and Permissions

The Anatomy of a Secure Request In Chapter 3, we explored how DRF's Request object standardizes incoming data through Unified Data Parsing and how the @apiview decorator processes incoming HTTP requests. But before DRF ever parses the request.data or negotiates content, it answers two fundamental questions: Who is making this request? and Are they allowed to do this? Authentication is the mechanism that associates an incoming request with a user. Permissions then take that authenticated user and determine if they have the authorization to perform a specific action (like a GET or POST). By default, DRF configures API endpoints with AllowAny, meaning authentication is bypassed entirely. To secure our API, we must explicitly define how clients prove their identity and what boundaries apply to that identity once verified. Implementing Token Authentication While Django’s default session authentication works perfectly for server-rendered templates, it is poorly suited for REST APIs. APIs are generally stateless, meaning the server should not maintain session state between requests. Furthermore, clients are often mobile apps or single-page applications (SPAs) that struggle to manage CSRF tokens tied to session cookies. TokenAuthentication solves this by issuing a unique, opaque string (a token) to a user upon login. The client then passes this token in the Authorization header of subsequent requests. Configuring the Token Scheme To implement TokenAuthentication, you first need to add the necessary DRF apps and classes to your Django project. 1. Add restframework.authtoken to your INSTALLEDAPPS. 2. Update RESTFRAMEWORK settings to make it the default authentication. With IsAuthenticated set globally, any view that doesn't explicitly override this will now reject unauthenticated requests with an HTTP 401 Unauthorized status code. Token Generation and Usage Tokens must be generated and handed to the client. For a standard setup, you can expose DRF’s built-in obtainauthtoken view, which takes a username and password via POST and returns a token. Once the client has the token, they include it in the headers for every request: Real-World Example: A Document Management API Let’s apply this to a concrete scenario. We are building a Document Management API where users store sensitive PDFs. We need an endpoint where users can generate a token, and a viewset to manage their documents. Notice the getqueryset override. While IsAuthenticated ensures only logged-in users access the view, it does not prevent User A from requesting User B's documents if they guess the primary key. Overriding getqueryset scopes the data to the requesting user, a fundamental pattern for securing multi-tenant data. Upgrading to JWT Authentication Standard tokens issued by DRF are stateless but monolithic. They never expire unless manually deleted from the database, and they carry no inherent metadata. JSON Web Tokens (JWT) improve on this by encoding user data …

6. Advanced Serialization and Relationships

Modeling Complex Reality Imagine an e-commerce platform where a customer submits a single POST request to create an order. That payload doesn't just contain an order ID; it includes the shipping address, a list of purchased items, and references to existing product variants. If your serializer isn't equipped to handle this, you are forced to make multiple API calls from the client—first creating the address, then the order, then looping through to create each item. This is slow, error-prone, and breaks transactional integrity. In Serializers for Data Conversion, we established how DRF translates complex Django querysets into JSON-compatible dictionaries. We used basic serializers to validate incoming request.data and return a Response. However, basic serializers only scratch the surface. Real-world data is deeply interconnected. As your API evolves from serving flat data to managing complex object graphs, you must learn to orchestrate how related models are read, computed, and written—all while avoiding circular references and performance traps. Navigating Nested Relationships for Reads When exposing related data, DRF provides several fields to traverse model relationships. For a read-only scenario, nesting is straightforward. If you have an Author model with a foreign key to a Profile and a many-to-many relationship to Book, you can represent these nested structures by instantiating serializers as fields. In this setup, DRF handles the traversal automatically. When Response Rendering occurs, the AuthorSerializer will dive into the related Profile instance and the Book queryset, serializing them into nested JSON objects. However, this ease of use introduces a significant risk: the N+1 query problem. If you query a list of 50 authors, DRF will query the database once for the authors, then 50 times for each author's profile, and 50 more times for their books. While we will cover deep performance optimization and caching in Chapter 9, it is critical to understand how to prevent circular references and query explosions at the serialization level right now. Preventing Circular References Deep object graphs often contain circular relationships. An Author has Books, and a Book has an Author. If you naively nest BookSerializer inside AuthorSerializer, and AuthorSerializer inside BookSerializer, DRF will enter an infinite loop when attempting to render the data, eventually crashing with a RecursionError. To break this cycle, you have two primary strategies: 1. Depth Limiting: Use the depth Meta attribute on one of the serializers to flatten the reverse relationship into a primary key reference, rather than instantiating a full nested serializer. 2. Directional Serialization: Design your serializers to flow in one direction. AuthorSerializer nests BookSerializer, but BookSerializer only returns the author's ID (or a minimal string representation), rather than nesting the full AuthorSerializer. By carefully structuring which serializer nests which, you maintain control over the payload size and …

7. Filtering, Searching, and Pagination

The Anatomy of an API Query Imagine querying an e-commerce database containing a million products. Without constraints, a single GET /products/ request would force the database to serialize and send every single record over the network. The result? A crashed API, an out-of-memory server, and a client application frozen while attempting to parse a 500MB JSON payload. As your Django REST Framework (DRF) APIs grow, the ability to retrieve specific data efficiently becomes critical. By default, the ViewSets and @apiview endpoints we built in previous chapters return the entire queryset. To build production-ready APIs, we must equip our endpoints with the intelligence to slice, dice, and limit data based on client requests. DRF handles this via backend classes. These are components that intercept the request after Authentication & Permissions run, but before the final Response Rendering phase. They modify the queryset in memory, applying SQL WHERE, LIKE, ORDER BY, and LIMIT clauses dynamically based on URL query parameters. Exact and Range-Based Filtering The most fundamental way to narrow down a dataset is through exact matches and range constraints. If a client wants all products in the "Electronics" category, or all products priced between $50 and $200, they need a way to pass these parameters to the database. Configuring DjangoFilterBackend DRF relies on the django-filter library to handle this heavy lifting. If you haven't already, install it via pip install django-filter. Then, add 'djangofilters' to your INSTALLEDAPPS in settings.py. To apply filtering globally, configure DRF's default filter backends: Alternatively, you can apply it on a per-view basis by defining the filterbackends attribute directly on your ViewSet. Applying Exact Filters Let’s look at a real-world scenario: an inventory management API. We have a Product model, and we want clients to filter products by category and isactive status. With this configuration, DRF automatically generates a FilterSet. A client can now send a request to GET /products/?category=Electronics&isactive=true. DRF intercepts the request, translates the query parameters into Django ORM filters (filter(category='Electronics', isactive=True)), and returns only the matching records. Custom Filtersets for Range-Based Queries Exact matching has its limits. What if a user wants to find products within a specific price range? For this, we need custom FilterSet classes. Suppose our e-commerce platform needs to allow filtering by a minimum and maximum price, as well as by the date the product was added to the inventory. Here, we define minprice and maxprice fields that map to the price attribute on the Product model. We use gte (greater than or equal to) and lte (less than or equal to) lookup expressions to create a range. We also leverage the ORM's relationship lookup (suppliername) to filter products based on the supplier's name. To use this custom filter, we …

8. API Testing and Debugging

It’s 2:00 AM. Your phone buzzes with an alert from your monitoring service: the production API is returning 500 Internal Server Error on the POST /api/orders/ endpoint. You push a hotfix, but you aren't entirely sure what caused the crash. Was it a missing field in the payload? A permission check failing on a related object? A validation error slipping past the serializer? The difference between guessing and knowing lies in your test suite. In the Django REST Framework pipeline, a request flows through content negotiation, authentication, view execution, and response rendering. A failure at any of these stages can trigger an error. Manually clicking through endpoints or relying solely on the browser to verify your API is unsustainable. This chapter transitions you from building APIs to proving they work. We will leverage DRF's APIClient to simulate complex HTTP requests, write comprehensive tests covering CRUD operations and permission enforcement, and use the browsable API alongside Django's logging to diagnose failures. The DRF APIClient Django provides a built-in TestCase and Client for testing views. However, DRF provides its own APIClient, which is specifically designed to handle the nuances of DRF views, such as automatic content negotiation and the request.data parsing we covered in earlier chapters. While Django's standard client requires you to manually format JSON payloads and set headers, the APIClient streamlines this. You can pass a Python dictionary directly to the data argument, and the client automatically formats it into a JSON payload, setting the correct Content-Type: application/json header. To use it, subclass django.test.TestCase and initialize the client in your setup method: Simulating Authentication Testing endpoints protected by Authentication & Permissions requires simulating logged-in users. The APIClient provides several methods to handle this: - client.login(username='...', password='...'): Works exactly like Django's standard test client. It hits the authentication backend and returns a session. - client.forceauthenticate(user=None): Bypasses the authentication backend entirely. This is highly recommended for unit testing because it is faster and allows you to test the view's logic independently of the authentication mechanism. - client.credentials(HTTPAUTHORIZATION='Token ...'): Used when you need to test the authentication layer itself, such as verifying that a specific JWT or Token actually grants access. To test unauthenticated requests, simply call client.forceauthenticate(user=None) to clear any previously authenticated users. Writing Comprehensive Endpoint Tests A robust test suite covers the full spectrum of CRUD (Create, Read, Update, Delete) operations, ensuring that your serializers and views interact correctly with the database. Let's look at a practical example using a Project model. We will test the creation and retrieval processes, paying special attention to how the request.data payload interacts with our serializers. Testing CRUD Operations When testing creation (POST), you should assert both the HTTP status code and the database state. …

9. Performance Optimization and Caching

Your API launched without a hitch. The test suite passes with flying colors, Authentication and Permissions are locked down, and your serializers elegantly handle complex nested relationships. But then the traffic hits. Users start complaining about sluggish load times, your database CPU spikes to 90%, and the logs reveal thousands of duplicate queries for a single API request. Functional correctness is only the baseline; high performance is what keeps an API alive in production. As your application scales, the way you query the database and serve responses becomes the difference between a 50-millisecond response and a 5-second timeout. Mitigating the N+1 Query Problem The most common performance bottleneck in Django REST Framework (DRF) is the N+1 query problem. Because DRF serializers iterate over querysets to convert model instances into JSON, they often trigger a separate database query for each related object they need to access. If you have a list of 100 books, and your serializer fetches the author's name for each book, DRF will execute 1 query to fetch the 100 books, and then 100 queries to fetch each book's author. That’s 101 queries (N+1, where N is the number of items). Solving N+1 with selectrelated Django provides two primary tools to force the database to fetch related data upfront: selectrelated and prefetchrelated. selectrelated follows foreign key relationships and performs a SQL JOIN at the database level. This means the related objects are included in the main query, reducing the total number of queries to exactly 1. Use selectrelated for ForeignKey or OneToOne relationships (the "one" side of a relationship). Solving N+1 with prefetchrelated prefetchrelated, on the other hand, performs a separate query for each relationship and joins them in Python. This is necessary because SQL JOINs multiply rows when dealing with reverse relationships or many-to-many fields, which can severely bloat the database payload. Use prefetchrelated for ManyToMany or reverse ForeignKey relationships (the "many" side). Practical Example: Optimizing a Book Catalog Endpoint Let’s look at a real-world scenario using a bookstore API. We have a Book model with a ForeignKey to Author, and an Order model with a ManyToMany to Book. If we use a standard BookViewSet, requesting /api/books/ will trigger an N+1 problem for both the author and the tags. We can resolve this entirely by overriding the getqueryset method in our ViewSet: By adding these two methods, a request for 100 books now executes exactly 3 queries total: one for the books, one for the authors (via JOIN), and one for the tags (via a separate IN lookup). Measuring Performance with django-debug-toolbar Never guess at performance. Use the django-debug-toolbar (in development) or DRF's built-in logging to verify your query counts. When testing the endpoint above, the toolbar …

10. Production Deployment and CORS

The Shift from Development to Production Your Django REST API works perfectly on your local machine. You have built robust Views and ViewSets, implemented complex Authentication and Permissions, and ensured your endpoints perform well using techniques from Performance Optimization and Caching. But when you deploy your API to a live server and try to access it from a frontend application hosted on a different domain, you hit a wall of errors. Two major factors cause this: the built-in development server (runserver) is fundamentally incapable of handling real traffic, and browsers actively block your frontend from making requests to your API due to Same-Origin Policy restrictions. Transitioning to a production environment requires replacing development conveniences with robust, secure, and scalable alternatives. This means configuring a production-grade WSGI/ASGI server, managing environment variables securely, handling static files, and implementing Cross-Origin Resource Sharing (CORS) to explicitly allow your web clients to interact with your API. Environment Variables and Settings Management In development, it is common to hardcode database credentials, secret keys, and DEBUG = True directly in settings.py. In production, this is a severe security vulnerability. Your settings must be dynamically configured via environment variables. Decoupling Configuration with django-environ The standard approach in Django is to use the django-environ package. It allows you to define a .env file locally for development, while your production server (or cloud provider) injects those same variables directly into the OS environment. First, install the package: Initialize it at the top of your settings.py: Securing the Essentials With environ set up, you must immediately secure your SECRETKEY and DEBUG settings. Leaving DEBUG = True in production will expose sensitive environment variables and stack traces to anyone who triggers an error. Database Connection Management Cloud databases (like AWS RDS or managed PostgreSQL) require robust connection handling. If your API experiences a surge in traffic, it can quickly exhaust the database connection pool. Django provides CONNMAXAGE to enable persistent database connections, reducing the overhead of establishing a new connection for every request. When using a connection pooler like PgBouncer, you often need to disable server-side cursors. Static Files in Production During development, Django handles static files automatically. In production, Django’s runserver is gone, and serving static files directly through Python is highly inefficient. You must configure Django to collect all static files into a single directory so a web server (like Nginx) or a cloud storage service can serve them directly. Configuring WhiteNoise For small to medium APIs, the WhiteNoise middleware allows your WSGI server to serve static files efficiently without requiring a dedicated Nginx configuration for static assets. Install it via pip install whitenoise, and add it to your middleware stack, placing it directly after Django's SecurityMiddleware: During your deployment …

Continue learning