Using Django REST Framework to Quickly Get a Working API: part II

Fabricio Saavedra
NicaSource
Published in
13 min readNov 9, 2022

In the first part of this three-part series of articles, we set up the base of a Django project to build an API leveraging some tools provided by the DRF. In this article, we’ll pick up where we left off. We’ll introduce you to the views responsible for request handling.

Views

Django uses a Model View, Controller (MVC) architecture. The Django dev team gives their interpretation of what this means for its project, but basically:

  • The model is how we organize information in the database. Django uses the model abstraction to establish how the database tables are organized and how they relate to each other.
  • The view describes the data that gets presented to the user. It’s not necessarily how the data looks but which data is presented. This is achieved with Python callback functions, such as the functions we specified in the urlpatterns variable.
  • The controller is the framework itself: the machinery that sends a request to the appropriate view, according to the Django URL configuration.

The rules the views use to decide what information needs to be presented to the user can be expressed by decorators or by class members if we’re using class-based views. And just like that, we got into naming one of the cool features that come from using the DRF: class-based views.

For fairly simple APIs, we use Django to perform repetitive CRUD operations on tables, that is, on rows in a relational database. Writing the instructions to perform those operations can quickly become time-consuming, and the statements themselves can end up being quite verbose, even with a concise programming language, like Python. To mitigate that, the DRF comes with a set of generic API views, classes from which you can inherit to avoid writing repetitive code by specifying class members to fit the needs of your business logic. As we have pointed out, views are functions, so to comply with this, we call the as_view method on the classes that inherit from the generics when we pass them as arguments to the path function call. Remember this?

If we go through the DRF documentation for API views, we will see that the DRF offers two ways to build a view:

  1. Class-based views (CBV from here on).
  2. Function-based views (FBV from here on).

Both take a Request object and return a Response object (instead of Django’s HttpRequest and HttpResponse respectively), but their use cases are different.

With FBV, what you see is what you get. Method handling is done by conditional branching and reusability is implemented by using decorators (not without some considerable limitations). For simple views, you can’t complain about them. Take this example from the documentation.

A FBV to handle snippets objects.

Class-Based Views and Serializers

You’ll want to use CBV to improve method separation and keep your code DRY. The views module from rest_framework provides the APIView class, which subclasses the Django View class. As we previously stated, the APIView class takes a Request object and returns a Response object (rigorously, the handle methods of the APIView class does that), but also API exceptions will be caught and mediated into appropriate responses. Last but not least, incoming requests will be authenticated, and appropriate permission and/or throttle checks will be run before dispatching the request to the handler method. This seems like a lot of features just from inheriting from the APIView class, but that is the beauty of using CBV, and they leverage on mixins and multiple inheritances. To illustrate that, we’ll come back to our project.

In part I of this series, we defined VehiclesList and CustomersList and we knew that they were CBV, but not much more than that. For this simple project, I’d write the following VehicleList class:

The class definition for the view.

Ok, what’s that? What is serializer_class? What is ListCreateAPIView? What is VehicleSerializer? That’s just 10 lines long, but many concepts are packed in there, so let’s break them down. Since generics it is imported from the rest_framework module, I think it’s fair to start from this point:

The class definition for ListCreateAPIView.

The docstring is pretty straightforward. That’s what it does, but how? We see that ListCreateAPIView has definitions for handling the GET HTTP method and the POST HTTP method. Both HTTP methods rely on instance methods (list and create), but we can’t see any definition for them directly. The class signature states that ListCreateAPIView inherits from ListModelMixin, CreateModelMixin, and GenericAPIView , so it seems logical to keep pulling the thread on them.

Class definition for ListModelMixin.

Ok, now we know where that list instance method came from in the ListCreateAPIView class. Just by reading the method definition, we can get an idea of what it does. Somehow, we get a queryset and then filter it to store it in the queryset variable. If the paginate_queryset method returns a truthy value, the list method returns a paginated response of the serializer data, which, oversimplifying, is the queryset in an understandable format for Javascript. If page is a falsy value, we return a non-paginated response with the serializer data. We still haven’t got a formal definition of what a serializer is, but that was some nice piece of self-documented code. Notice that we don’t see any definition for get_serializer, get_queryset, filter_queryset, and so on, but given that this is a mixin, it makes sense.

The class definition for CreateModelMixin.

Intuitively, the CreateModelMixin would be the place where we could find a definition for the create instance method. It gets a serializer, but instead of passing a queryset or a page to the get_serializer method, it passes a data keyword argument with the request data as a value. Then it validates the serializer, saves it (assuming everything is ok with the validation), and returns the serializer data in an Response object. Why do we have to run a validation on the data in this case? We’ll get to that later on.

Finally, if we inspect the GenericAPIView class, we’ll see this:

Class definition for GenericAPIView.

Ok, that’s a lot, but you can see that this class has the method definitions we saw both in ListModelMixinand CreateModelMixin. Let’s inspect the most important ones.

get_queryset function definition.

get_queryset makes an initial check on the presence of a queryset class member. If it exists, it checks that it is in fact, a queryset, evaluates, and returns it. I compressed the docstring for readability, but it basically says why we need to get the queryset using this method, especially if we’re overriding the method handlers. What about get_serializer?

get_serializer function definition.

Docstring gave it all away here, get_serializer takes whatever arguments it receives and generates a serializer instance with that. Calling get_serializer_class just returns the serializer_class class member defined in the CBV that inherits from GenericAPIView, after asserting it is not “None”.

Still, what is this serializer we’ve been mentioning? The docstring indicates that a serializer instance should be used for validating and deserializing input, and for serializing output. Furthermore, the documentation indicates:

Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.

It couldn’t be clearer than that. The VehiclesList CBV has two HTTP method handlers.

  • When it receives a GET request, the get function will perform a list the operation, which is meant to read information or strictly talking, serializing output, from Django querysets and objects to Python datatypes, which can be rendered into JSON (as to say, understandable to a React app).
  • On the other hand, when it receives a POST request, the post function will perform a create operation, which “creates information” by validating input and creating Django model instances.

So, by defining a serializer class and passing information to it, we can:

a. Convert Django objects and Python datatypes to JSON, XML, etc.

b. Convert input into a Django object if said input complies with the validation

In our brief but conceptually strong example, we defined the serializer_class class member with, but didn’t get to see how the latter was defined.

VehicleSerializer serializer definition.

We’re clearly taking advantage of the DRF since we’re importing from rest_framework the serializers module, and then inheriting from the ModelSerializer class attribute. Another peek at the documentation will shed some light on what a ModelSerializer is:

Often you’ll want serializer classes that map closely to Django model definitions.

The ModelSerializer class provides a shortcut that lets you automatically create a Serializer class with fields that correspond to the Model fields.

So the ModelSerializer comes in really handy when we want to serialize and deserialize information in fields which resemble a Django Model we have defined.

In the Meta class, we define some basic information to tell the serializer how it should behave (actually this is a Django pattern, not a serializer thing since we can define Meta for a model definition too). In this case, we specify that the model we want to map is Vehicle. The fields Meta member will specify which model fields we want the serializer to use by defining a tuple or list of strings with field names. You can set this attribute to the special value __all__ to say that all model fields should be used. Since version 3.3.0, it’s mandatory to define this class member (or its counter-part exclude).

A ModelSerializer provides three really useful features:

  1. It will create a series of serializer fields from the model.
  2. It will include a validator for each of the fields.
    — Do you remember that when we were using the create method of the CreateModelMixin, it had to run a validation on data? That’s because, for new objects, it needs to make sure the input data doesn’t break the rules for the target model.
  3. It will include default implementations for create and update (the former will be called if we’re not passing an existing instance to the serializer class along with the request data, and the latter if we do).

Before we test the functionality of our VehiclesList view (that sounds very far at this point, right?), we need to update the Vehicle model. We’d like to illustrate something cool about serializers. But first, we need to add a field:

Updated definition of Vehicle model.

In the initial definition of Vehicle, we didn’t say “we want this vehicle to be related to this customer”. Imagine the mechanic shop has wealthy customers. There is a chance that a customer will have many vehicles, but a vehicle can only be owned by a customer. This kind of relation is a one-to-many relationship, and it’s commonly represented by using a ForeignKey field. In relational databases, it means a row in the table customers is related to many rows in the table vehicles. Since we’re altering the Vehicle model definition, we need to generate a migration file to communicate this change to the database schema. Then migrate will commit the newest migration files:

python manage.py makemigrations
python manage.py migrate

By now, you should be able to start a development server by running:

python manage.py runserver
Successful start of the development server.

By default, this development server runs on port 8000. If you’re getting a log similar to the one in the picture above, it means your server started without errors, but that doesn’t mean it won’t crash when we actually make requests to it.

We will use Postman to see what happens when we make some requests to the paths we have previously exposed. You can use curl or any other utility to make the request, but for the sake of illustration, I’ll prefer a GUI.

GET request for /api/vehicles.

If you remember, making a GET request to the /api/vehicles path performed a list operation from the VehiclesList CBV, returning the serialized data from the queryset of that view. Since we didn’t create any vehicle, it’s natural to receive an empty array as a response. It’s in our hands to have the GET request to return something more interesting. Let’s try to make a POST request to the same path, but we’re going to have an empty body for this attempt to see what happens.

POST request to /api/vehicles/ with empty body.

That’s a very informative response. We got a 400 status code (bad request) and an indication of the fields required to make a successful request. At this point, it’s only saying “we need you to define these fields” but it doesn’t say anything about the data types for the values of those fields. Let’s send a more convenient POST body.

POST request to /api/vehicles/ with validation complying data.

That returned a 200 status code and a serialized representation of the Django object created. We see that it automatically added a id field with a value 1 for it. That means we did a successful insertion in the vehicles table and that the information was saved in a row with an id equal to 1. Django has that default behaviour, and you can review or update that in the VehiclesConfig class that was generated when we started the vehicles service/app, or globally if you want to change the behaviour across the entire project.

Ok, that seems pretty easy, we send the fields required, and we create a Django object, right? Let’s try to insert another car, but this time let’s pass the kilometers as a string, and the last oil check date in another format (MM/DD/YYYY):

POST request to /api/vehicles/ with different input.

Bad request on sight! The built-in validation from using a ModelSerializer is very clear, the last_oil_check datetime field does not accept that format right out of the gate. It didn’t bother about passing the kilometers as string, presumably because it did a type cast, but it had its concerns about the datetime format, so let’s accommodate that:

POST request to /api/vehicles/ with corrected payload.

Another successful insertion! Two things to notice here. First, it inserted the second object with id equal to 2. Cool! In the first insertion and this one, we received responses customer equal to null. That’s because the customer field we defined in the Vehicle model took blank=True and null=True as arguments, or, in other words, we didn’t make it a required field. We didn’t create a single customer before making the POST request to the vehicles endpoint neither, so we wouldn’t know what to send there. Still, being a SQL-type database, we can’t omit that “column” when we save the data.

If you were following along and you try a GET request now, things will look different:

GET request to /api/vehicles.

Those ten lines of code in the vehicles/views.py module, plus the serializer definition, gave us the ability to list and create Vehicle objects. For simple operations, using the appropriate generic view will be enough most of the time.

What if we try to define a customer while inserting a vehicle? We must define the CBV for listing and creating Customer objects first, but that’s pretty similar to what we’ve done for the Vehicle objects, so I won’t write the details and you can take that as an exercise or check the repo to make sure you got the idea. Once we defined the serializer class and the CBV CustomersList we can make an insertion like this:

POST request to /api/customers/

Ok, now that we successfully inserted an Customer object, how would we specify that a vehicle belongs to a certain customer while inserting? We know that Vehicle it has a foreign key field for customer, so maybe we can try the following request body:

{
"name": "Civic",
"brand": "Honda",
"kilometers": 7000,
"last_oil_check": 2021-12-31,
"customer": 1
}

If you make a POST request to /api/vehicles/ with that body, you’ll get a 201 status code. Don’t believe me?

POST request to /api/vehicles/ defining a customer.

Yeah, it worked, but wouldn’t it be more intuitive to define customer_id = 1 while specifying the relationship? Probably, but by default, while using the ModelSerializer, serializer fields and relationships will be automatically generated for you. ForeignKey fields use the PrimaryKeyRelatedField serializer field by default, so that’s the reason why we directly pass an id to customer. Of course, you could override this behaviour to pass a customer_id, and exclude the automatically generated customer serializer field.

Another pertinent question: is it useful to receive a response with customer represented by an id? It’d be more practical to receive a response with more descriptive information about the customer (name, address, etc). You can write nested representations to achieve that, but that would require us to override the get method in the CBV to use a different serializer class from the one used to perform create operations, and I believe that would be a little out of the scope of the present article.

That was a lot. We need to digest this as if it was a big plate of spaghetti. Just a last question playing the part of a digestive expresso. Do you think it is cool to have your API exposed so that anyone with its URLs can get to read and insert data in your database? Most definitely, it is not. Authentication and permissions are the next and last topic in this three-part series on building an API with DRF, so stay tuned!

--

--