Django REST Framework by Example

The full source code for the post below is on GitHub.

One of the best ways to get quickly up to speed with a new framework is to practice learning the design patterns and mechanisms for the flow of data between components.

For Django, the popular Python web framework, the design pattern is MVT. Model, View, Template.

For Django REST Framework, the pattern is Model, View, Serializer. From now on, I'm just going to refer to the Django REST Framework as DRF.

I like to create project skeleton's while practicing application development. This really helps me when scaffolding a client's project. I simply fork the repo and I have a great starting application. I can just drop in the models, create the views and migrate the database. Wire up the routes, configure settings and you are good to go.

It's been a while since I developed a DRF API for a customer. It was really more of an add-on component to an existing Django front-end app, I added DRF ModelViewSet and some custom routes for features beyond the basic API generic views. I also included Swagger UI to document the API endpoints. This documentation, along with the django_swagger library, creates a cool API endpoint test environment.

If you didn't know this about me, I love to Geocode, so my example for this post will be the locations module from the project's source. For complete source code, see the project repository.

For the sake of brevity, I am only going to include the files from the locations module.

The Model:

When geolocating an IP address with the GeoCityLite database, this is the basic data model. I added the IP to the model to save and include in the Response.

The Serializer:

One of the most basic and important parts of developing APIs is data serialization. Django and the REST framework make it a breeze to serialize data in both directions.

The View:

When the router detects a request at this path, /locations/geolocate/<str:ip_addr>, the view that is bound to this route is called and is passed the request's variables.

The geolocate API view function accepts the request, parses the variables and performs the geo-lookup on the supplied input string. Note, you will need to download the GeoCityLite database from MaxMind. Set it up at the path in the script. /var/lib/geoip/

Using the ipaddress Python library, the first try/except attempts to set the type of the input to type IPv4Address. If the input string is not the correct type, the request will execute an Exception for an AddressTypeError and pass the Response the appropriate error handler status, in this case HTTP_400_BAD_REQUEST. If the supplied string is of type and format IPv4Address, we call the GeoIP record_by_addr method and set the value of our return variable, gi_lookup.

If the geo-lookup is successful, I update the gi_lookup dictionary with the IP addressed passed into the request.

Next, I pass the updated gi_lookup dict to the LocationSerializer. Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes. These types can then be easily rendered into HTML, JSON, XML or any other of the many available content types. Serializers also allow for deserialization of the data as well, parsing the data to be converted back into the more complex data types, after first validating the incoming data.

If the geolocate is successful, and the serializer validates the incoming data from the geo-lookup, then serializer.save() is called which calls serializer.create(), the database record is added, and the Response returns the serializer.data and an HTTP_201_CREATED status.

If the validation of the serialized data fails, then the Response is passed the gi_lookup dict and an HTTP_200_OK status,

If the geolocation fails, the Response is passed the input string IP address and a HTTP_200_OK status.

The Router:

The router simply responds to incoming requests, maps the defined routes to a specific view and passes the views the request variables. For our example, the variable we need for the view to perform the geolocation is a keyword argument <str: ip_addr>

The Admin:

I ran some test IP's against the API geolocate function in my Python shell.

import csv  
import requests

def main():  
    url = 'http://localhost:8888/locations/geolocate/'
    hdr = {'user-agent': 'SimplePythonFoo()', content-type': 'application/json'}
    api_method = 'GET'
    user = ('webmin', 'password')

    try:
        with open('ip_data.csv', 'r') as f1:
            reader = csv.reader(f1, delimiter=',')
            next(reader)
            for row in reader:
                try:
                    r = requests.request(
                        api_method, url + row[0], headers=hdr, auth=user)
                    print(r.content)
                except requests.HTTPError as http_err:
                    print('API returned: {}'.format(str(http_err)))
    except IOError as io_err:
        print('Can not access the source data file: {}'.format(str(io_err)))

My results:

Requests:

[08/Mar/2019 20:05:37] "GET /locations/geolocate/70.139.127.217 HTTP/1.1" 201 329
[08/Mar/2019 20:05:38] "GET /locations/geolocate/66.87.144.198 HTTP/1.1" 201 329
[08/Mar/2019 20:05:38] "GET /locations/geolocate/208.54.85.171 HTTP/1.1" 201 327
[08/Mar/2019 20:05:38] "GET /locations/geolocate/172.58.152.63 HTTP/1.1" 200 309

Responses

{"id":2342,"ip_addr":"70.139.127.217","time_zone":"America/Chicago","latitude":29.790800094604492,"longitude":-95.10910034179688,"region":"TX","region_name":"Texas","city":"Channelview","country_name":"United States","country_code":"US","country_code3":"USA","postal_code":"77530","dma_code":618,"area_code":281,"metro_code":618}

{"id":2343,"ip_addr":"66.87.144.198","time_zone":"America/Chicago","latitude":44.9994010925293,"longitude":-93.2969970703125,"region":"MN","region_name":"Minnesota","city":"Minneapolis","country_name":"United States","country_code":"US","country_code3":"USA","postal_code":"55411","dma_code":613,"area_code":612,"metro_code":613}

{"id":2344,"ip_addr":"208.54.85.171","time_zone":"America/New_York","latitude":28.514299392700195,"longitude":-81.44290161132812,"region":"FL","region_name":"Florida","city":"Orlando","country_name":"United States","country_code":"US","country_code3":"USA","postal_code":"32811","dma_code":534,"area_code":407,"metro_code":534}

{"country_code":"US","country_code3":"USA","country_name":"United States","region":"NC","city":null,"postal_code":null,"latitude":35.22710037231445,"longitude":-80.84310150146484,"region_name":"North Carolina","time_zone":"America/New_York","dma_code":0,"metro_code":0,"area_code":0,"ip_addr":"172.58.152.63"}

Notice that the last request for IP 172.58.152.63 returned a status 200, and not a 201 CREATED, meaning it geo-located but did not validate when serialized. The database did not save this record.

Wrapping Up

Do you love Django? The Django REST Framework makes it dead simple to create super powerful APIs with very little effort. Using the same design patterns as Django, developers can spend less time writing code and more time building and polishing their apps. It's a matter of understanding the design patterns and knowing what's going on behind the scenes. Interested in learning more, drop me a line or leave a comment. The full source code for the post above is on GitHub.

Craig Derington

Secular Humanist, Libertarian, FOSS Evangelist building Cloud Apps developed on Red Hat Enterprise Linux and Ubuntu Server. My toolset includes Python, Celery, Flask, Django, MySQL, MongoDB and Git.

comments powered by Disqus