PyCharm 2024.2 Help

Building APIs With Django REST Framework

Enable the Django plugin

This functionality relies on the Django plugin, which is bundled and enabled in PyCharm by default. If the relevant features are not available, make sure that you did not disable the plugin.

  1. Press Ctrl+Alt+S to open settings and then select Plugins.

  2. Open the Installed tab, find the Django plugin, and select the checkbox next to the plugin name.

In this tutorial, we will create a rental platform API with Django REST framework.

Before you start

Make sure that the following prerequisites are met:

  • You are working with PyCharm version 2023.3 or later. If you still do not have PyCharm, download it from this page. To install PyCharm, follow the instructions, depending on your platform.

  • General understanding of the concept of RESTful APIs.

  • Previous experience with Python and Django (you can start with Django tutorial).

This tutorial has been created with the following assumptions:

  • Python 3.11

  • Django 5.1

  • Django REST framework 3.15.2

Setting up a Project

  1. Go to File | New Project, or click the New Project button in the Welcome screen. The New Project dialog opens.

  2. In the New Project dialog, do the following:

    • Specify project type Django.

    • Specify the project name (api_tutorial_rental in our example).

    • Choose the type of virtual environment for your project (we will use project venv).

    • Expand the More Settings section and provide the application name (rental).

    New Project dialog
  3. When you click Create, PyCharm will set up the project and install Django in the project environment.

    The Django REST framework package needs to be installed manually. Open the Python Packages tool window by clicking its icon on the left. Search for the djangorestframework package and install the latest version.

    Install 'djangorestframework'
  4. Now we need to update INSTALLED_APPS in settings.py:

    • Open Search Everywhere (double Shift). Go to Symbols tab and type the first letters of the desired symbol (variable, class, etc.) to find it, for example, “insapp”.

    • Press Enter to jump to the desired symbol and add ‘rest_framework’ to INSTALLED_APPS.

    Add ‘rest_framework’ to ‘INSTALLED_APPS’

Creating serializers

In general, serializers are used to “translate” Django model instances or query sets into other formats, usually JSON or XML, so that they can be sent in the body of an HTTP response. Serializers also provide deserialization when text data from an HTTP request is parsed, validated, and converted into a model instance.

The processes of serialization and deserialization are crucial for any API, and Django REST framework can take it over completely. Thanks to its ModelSerializer class, we can generate a serializer for any model in just two lines of code.

Writing a model-based serializer

  1. Use Search Everywhere or the Project tool window (Alt+1) to open rental/models.py, and copy the following code into the editor:

    from django.db import models SIZE_CHOICES = [ ('ST', 'Studio'), ('1BR', '1 bedroom'), ('2BR', '2 bedrooms'), ('3BR', '3 bedrooms'), ('MBR', '3+ bedrooms'), ] TYPE_CHOICES = [ ('H', 'house'), ('APT', 'apartment'), ] class Offer(models.Model): created = models.DateTimeField(auto_now_add=True) address = models.CharField(max_length=100, blank=True, default='') size = models.CharField(choices=SIZE_CHOICES, default='1BR', max_length=100) type = models.CharField(choices=TYPE_CHOICES, default='APT', max_length=100) price = models.PositiveIntegerField(default=0) sharing = models.BooleanField(default=False) text = models.TextField(default='') class Meta: ordering = ['created']

    Note that all fields of the Offer model have defaults, which means that we can create an instance without providing any field values. Additionally, we’ve provided choices for the size and type fields.

  2. Now let’s run migrations. Open PyCharm’s manage.py console (Ctrl+Alt+R) and execute the following commands:

    • makemigrations - scans your models for any changes and creates new migration files to reflect those changes in the database structure.

    • migrate - applies the migration files to the database, updating its schema to match the current state of your models.

    Run migrations
  3. Now we need to create serializers.py in the rental directory. Right-click the directory in the Project tool window (Alt+1), go to New | Python file, and specify “serializers” as the file name.

    Add Serializers

    The newly created file opens in the editor. Fill it with the following code:


    from rest_framework import serializers from rental.models import Offer class OfferSerializer(serializers.ModelSerializer): class Meta: model = Offer fields = ['id', 'address', 'size', 'type', 'price', 'sharing', 'text']
    Add code to 'serializers.py'

    As you can see, OfferSerializer inherits from the ModelSerializer provided by a Django REST framework and is defined with only two lines of code. On line 6 we’ve specified the base model (which is imported from rental/models on line 2), while line 7 contains the list of model fields to be serialized.

Using the serializer to save data

Let’s use the serializer to add data into the database.

  1. Open the Python console by clicking the corresponding icon on the left and run the following code in it:

    from rental.models import Offer offer = Offer(text='A cozy space in "loft" style.\nPerfect for young couples') offer.save() offer = Offer(text='A warm house for a big family') offer.save()
    Use serializer in the Python console

    We’ve created two instances of the Offer model and saved them into the database by using the built-in save() method.

  2. Now let’s open the database. Your project contains db.sqlite3, which can be opened either from the Project tool window or by using Search Everywhere. When you open the database for the first time, PyCharm will register it as the project data source. The Data Sources and Drivers window will open.

    Click Test Connection. If you see a warning saying that you need to install, update, or switch the database driver, perform the required action. Then click OK to finish adding the data source to the project.

    Database connection
  3. When the Database tool window opens, expand the structure of the db data source until you see the rental_offer table.

    Rental_offer table

    Click it to browse its contents in the editor.

    Rental_offer database contents

    As you can see, there are two records in the database table now. We didn’t provide values in any of the fields except text, which is why the default values from the model definition have been used.

Providing REST API logic

Writing function-based views

Now we want the API to be able to add rental offers automatically based on the incoming requests.

  1. Let’s start creating the API’s logic in rental/views.py. Open the file and fill it with the following code:

    from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from rental.models import Offer from rental.serializers import OfferSerializer @api_view(['GET', 'POST']) def offer_list(request): if request.method == 'GET': offers = Offer.objects.all() serializer = OfferSerializer(offers, many=True) return Response(serializer.data) elif request.method == 'POST': serializer = OfferSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    Code in 'rental/views.py'

    We’ve added a function-based view called offer_list. It will be used to provide information about available rental offers, as well as to add new offers to the database. Here’s what’s inside:

    • @api_view (line 7) is the Django REST framework decorator for function-based views. GET and POST are the methods accepted by this view.

    • If the request method is GET, a queryset with all offers in the database is created (line 10) and serialized (line 11). In this case, the body of the response contains data about all available offers in JSON form. The response is sent with the default status code (200 OK).

    • If the request method is POST, OfferSerializer is used to deserialize data from the request (line 14). If the data is validated successfully (line 15), it’s saved to the database (line 16). The response contains the saved data and has the status code 201 Created (line 17).

    • If validation fails, the API will return the error info with the status 400 Bad Request.

  2. It would also be useful if we could obtain information about any specific offer, edit that information, and remove offers from the database. Let’s add another view and call it offer_detail:

    @api_view(['GET', 'PUT', 'DELETE']) def offer_detail(request, pk): try: offer = Offer.objects.get(pk=pk) except Offer.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == 'GET': serializer = OfferSerializer(offer) return Response(serializer.data) elif request.method == 'PUT': serializer = OfferSerializer(offer, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': offer.delete() return Response(status=status.HTTP_204_NO_CONTENT)
    Code in 'rental/views.py'

    This view accepts three methods (GET, PUT, and DELETE) and works as follows:

    • First of all, it checks whether the offer whose ID has been specified in the pk parameter exists in the database (line 23). If it doesn’t, 404 Not Found is returned.

    • For GET requests, the API serializes the offer data (line 28) and returns it in the response body.

    • For PUT requests, the API serializes the offer data from the database and merges it with the data from the request body (line 32). If the validation is successful (line 33), the updated offer is saved to the database (line 34) and returned in the response body (line 35). Otherwise, the error info is returned with the 400 Bad Request status.

    • Finally, for DELETE requests, the API deletes the offer and returns 204 No Content.

    Now that we’ve defined the API’s logic, we have only one step left before we can use our API. We need to define Django URLs, also known as API endpoints.

Defining and testing API endpoints

  1. Let’s start by creating urls.py in the app directory and filling it with the following code:

    from django.urls import path from rental import views urlpatterns = [ path('offers/', views.offer_list), path('offers/<int:pk>/', views.offer_detail), ]

    We’ve defined two endpoints for our two views.

  2. Include rental/urls .py in the existing project’s urls.py file:

    from django.urls import path,include urlpatterns = [ path('', include('rental.urls')), ]
  3. Let’s open the Endpoints tool window. In case you haven’t used it before, you can find it under the menu on the left.

    Open Endpoints tool window

    The tool window displays all available endpoints and methods.

    Endpoints tool window
  4. Before testing the API, make sure that the Django server is running. On project creation, PyCharm automatically set up the run configuration. Just launch it from the Run widget in the window header. Note the server address (usually, localhost) and port number in the Run tool window that opens.

    Run tool window
  5. Let’s go back to the Endpoints tool window. Select /offers/ in the list and switch to the HTTP Client tab at the bottom. Edit the port number if needed and then click Submit Request.

    HTTP Client tab

    PyCharm runs the request and saves the response body to a file. You can either scroll up to explore the response or click the link to open the file in the editor.

    Response body saved to a file
  6. Let’s submit a DELETE request to remove the second offer (DELETE http://localhost:8000/offers/2/) and then submit another request for the list of available offers (GET http://localhost:8000/offers/). Now only one offer is available.

    Response body after DELETE
  7. Another feature of Django REST framework is its browsable API. Opening http://localhost:8000/offers/ (edit the port number if needed) in your browser brings you to a page where you can view the list of available offers and add new ones.

    Page in browser

Implementing generic class-based views

When speaking about the amazing features of Django REST framework, it’s impossible not to mention generic class-based views.

  1. Let’s use them to rewrite the code in rental/views.py:

    from rest_framework import generics from rental.models import Offer from rental.serializers import OfferSerializer class OfferList(generics.ListCreateAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer class OfferDetails(generics.RetrieveUpdateDestroyAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer

    Now each view is just three lines of code! You only need to worry about choosing the right generic class to inherit from.

  2. As we’re not using feature-based views anymore, we need to update rental/urls.py:

    urlpatterns = [ path('offers/', views.OfferList.as_view()), path('offers/<int:pk>/', views.OfferDetails.as_view()), ]
  3. Let’s attempt to submit invalid data to see how API validation works.

    Go to the Endpoints tool window. Now there are additional OPTIONS and PATCH methods, which come from generic views. Select /offers/ from the list of endpoints and click Open in Editor. PyCharm creates an .http file and copies the endpoint into it. Provide the request body in JSON format and submit the request.

    POST http://localhost:8000/offers/ Content-Type: application/json { "address": "", "size": "8BR", "type": "H", "price": 1000000, "sharing": true, "text": "A spacious villa for a large family." }
    Open in Editor

    In the Services tool window that opens, you’ll notice that the response has the 400 Bad Request status. Click the link to open the JSON file with the response.

    400 Bad Request response
  4. As you can see, the offer hasn’t been added, because we specified the wrong value in size. According to the Offer model, we should use MBR when there are more than 3 bedrooms. Let’s edit the request and submit it again.

    Request response

Enabling authentication and permissions

At the moment, anyone who knows the endpoint address can add, edit, and remove offers. This is not a normal situation in the real world. Normally, you’d like to have control over who can do what with your API. That can be achieved by implementing authentication and permissions.

Introducing users

  1. First, we need to introduce the concept of users. Let’s start by adding the author field to the Offer model:

    author = models.ForeignKey('auth.User', related_name='offers', on_delete=models.CASCADE)

    This field has the ForeignKey type, which means that it’s used to represent the relationships between offers and the users who create them.

  2. As we’ve updated the model, we need to reset the database and recreate the rental_offer table in it, now with the author field. To achieve this, perform the following steps:

    • Open the manage.py console (Ctrl+Alt+R) and run the following commands one at a time:

      > flush > migrate rental zero
    • In the rental/migrations directory, remove all migrations, keeping only __init__.py.

    • Then continue in the manage.py console:

      > makemigrations > migrate
  3. To make sure that you are ready to proceed, go to the Database tool window and open the rental_offer table. It should have the author_id column.

    Then open rental/serializers.py and add UserSerializer. We will use Django’s built-in authentication system, so we will import the existing User model and update OfferSerializer to comply with the newly added author field:

    from rest_framework import serializers from rental.models import Offer from django.contrib.auth.models import User class OfferSerializer(serializers.ModelSerializer): author = serializers.ReadOnlyField(source='author.username') class Meta: model = Offer fields = ['id', 'address', 'size', 'type', 'price', 'sharing', 'text', 'author'] class UserSerializer(serializers.ModelSerializer): offers = serializers.PrimaryKeyRelatedField(many=True, queryset=Offer.objects.all()) class Meta: model = User fields = ['id', 'username', 'offers']
    Introducing users 'serializer.py'
  4. We also need to define two new views in rental/views.py: one for managing the list of all users and another one for user details. The User model should be imported here, too, and don’t forget to import the newly created UserSerializer from serializers.py as well. Also, update the OfferList class to override the default perform_create() method so that the additional author field is passed when creating an offer.

    from rest_framework import generics from rental.models import Offer from rental.serializers import OfferSerializer, UserSerializer from django.contrib.auth.models import User class OfferList(generics.ListCreateAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer def perform_create(self, serializer): serializer.save(author=self.request.user) class OfferDetails(generics.RetrieveUpdateDestroyAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer class UserDetails(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer
    Introducing Users 'views.py'
  5. Add the following endpoints for users to rental/urls.py:

    from django.urls import path from rental import views urlpatterns = [ path('offers/', views.OfferList.as_view()), path('offers/<int:pk>/', views.OfferDetails.as_view()), path('users/', views.UserList.as_view()), path('users/<int:pk>/', views.UserDetails.as_view()), ]
    Introducing Users 'urls.py'

Making authentication required

Now we need to ensure that only authenticated users are able to add offers through the API.

  1. Update both the OfferList and OfferDetails views with the following properties to set permissions. Authenticated users will be able to add and edit offers, and others will be able to view them:

    from rest_framework import generics from rental.models import Offer from rental.serializers import OfferSerializer, UserSerializer from django.contrib.auth.models import User from rest_framework import permissions class OfferList(generics.ListCreateAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] def perform_create(self, serializer): serializer.save(author=self.request.user) class OfferDetails(generics.RetrieveUpdateDestroyAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer class UserDetails(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer
    Making authentication required 'views.py'
  2. To make sure that things work as expected, let's run a POST request without authentication:

    POST http://localhost:8000/offers/ Content-Type: application/json { "address": "", "size": "1BR", "type": "APT", "price": 350000, "sharing": false, "text": "A small modern flat. Central location." }

    You should get a 403 Forbidden response.

    Try to make a request without authentication
  3. Let’s create users. Go to the manage.py console and run the createsuperuser command. Remember the username and password you provide.

  4. Before proceeding to the next step, you’ll need the Base64-encoded string consisting of the username and password joined by a single colon. For example, we’ve created ‘admin’ with the password ‘pass123’ (merely as an example; in real life, you should always use a much stronger password). Open the Python console and run the following, replacing ‘admin:pass123’ with your user credentials:

    >>> import base64 >>> base64.b64encode(b'admin:pass123')
    Admin user creation
  5. Now let’s run the same request but with the Authorization header.

    POST http://localhost:8000/offers/ Authorization: Basic YWRtaW46cGFzczEyMw== Content-Type: application/json { "address": "", "size": "1BR", "type": "APT", "price": 350000, "sharing": false, "text": "A small modern flat. Central location." }

    You should get a 201 Created response.

Elaborating on permissions

At the moment, any authenticated user can edit any offer. Let’s set up permissions so that offers can only be edited by their authors.

  1. Create rental/permissions.py and fill it with the following code:

    from rest_framework import permissions class IsAuthorOrReadOnly(permissions.BasePermission): def has_object_permission(self, request, view, obj): return request.method in permissions.SAFE_METHODS or obj.author == request.user

    The IsAuthorOrReadOnly class is subclassed from the Django REST framework BasePermission class. Permission is unconditionally granted if the request method is one of the SAFE_METHODS, which are GET, HEAD, and OPTIONS. Otherwise, the requesting user should be the offer’s author in order to get permission.

  2. Go to views.py, import the newly created permission, and update permission_classes in OfferDetails:

    from rest_framework import generics from rental.models import Offer from rental.serializers import OfferSerializer, UserSerializer from django.contrib.auth.models import User from rest_framework import permissions from rental.permissions import IsAuthorOrReadOnly class OfferList(generics.ListCreateAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] def perform_create(self, serializer): serializer.save(author=self.request.user) class OfferDetails(generics.RetrieveUpdateDestroyAPIView): queryset = Offer.objects.all() serializer_class = OfferSerializer permission_classes = [ permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly ] class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer class UserDetails(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer
    Elaborating On Permissions 'views.py'
  3. Now create another user by running createsuperuser in the manage.py console (we’ll use ‘jetbrains:jet123’). Then submit the following request to update the offer with ID 1 (created by the admin user):

    PUT http://localhost:8000/offers/1/ Authorization: Basic amV0YnJhaW5zOmpldDEyMw== Content-Type: application/json {"text":"A small modern flat. Very central location."}

    You should get 403 Forbidden with “You do not have permission to perform this action” in the response details.

    Try to violate permissions
  4. Then try the same but with admin's credentials:

    PUT http://localhost:8000/offers/1/ Authorization: Basic YWRtaW46cGFzczEyMw== Content-Type: application/json {"text":"A small modern flat. Very central location."}

    You should get 200 OK.

  5. Let’s check if there is authentication in the browsable API. Open http://localhost:8000/offers/1/ (edit the port number if needed) in the browser. There’s no longer a form associated with the POST method, and the DELETE button is gone as well. We need to enable the login page to be able to use browsable API for such operations.

    Go to the project's urls.py and update it as follows:

    from django.urls import path, include urlpatterns = [ path('', include('rental.urls')), path('api-auth/', include('rest_framework.urls')), ]
    Elaborating On Permissions 'urls.py'

    Now update the page in the browser. You should see Log in in the upper right corner. Click it and enter credentials of one of the previously created users to be able to perform actions on offers.

Conclusion

By completing this tutorial, you have learned to implement the following features of Django REST framework:

  • ModelSerializer for creating serializers based on models.

  • Generic class-based views for writing API logic in a concise and idiomatic way.

  • Browsable API for easy visualization of available endpoints and data.

  • Django authentication system for configuring user permissions.

Last modified: 09 September 2024