Using Django REST Framework to Quickly Get a Working API: part II
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:
- Class-based views (CBV from here on).
- 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.
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:
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 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.
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.
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:
Ok, that’s a lot, but you can see that this class has the method definitions we saw both in ListModelMixin
and CreateModelMixin
. Let’s inspect the most important ones.
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
?
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 alist
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 acreate
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.
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 aSerializer
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:
- It will create a series of serializer fields from the model.
- It will include a validator for each of the fields.
— Do you remember that when we were using thecreate
method of theCreateModelMixin
, 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. - It will include default implementations for
create
andupdate
(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:
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
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.
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.
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.
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):
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:
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:
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:
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?
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!