A blog about cloud computing and development
About me

A REST API with Django and Django Tastypie

Introduction

If you've been through the excellent tutorial, Writing your first app with Django, or maybe you've gota Django app and want to add a REST API, then this tutorial is for you. One of the benefits of using Django is the wealth of packages available for it (look here).We'll be using one of those packages called django-tastypie. This package includes many features that come in handy, such as:

  • Authenication
  • Authorization
  • Multiple serialization formats
  • Throttling
  • Pagination
  • Validation
  • Caching

Getting started

The philosophy behind Tastypie is that resources should be able to be round tripped. That is, you should be able to serialize a resource (to JSON for example), sendit to a client, and the client should be able to send that object back (possibly with changes), and that serialized object should be able to be reconstructed back into a resource. Resources being sent to the client are 'dehydrated' and serialized objects being sent from a client are 'hydrated' into Resource objects. Note: If you are not familiar with Django models, work through this tutorial.

Example Note model

# models.py
"""
Example DB models
"""
from django.db import models
from django.conf import settings

class Note(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    title = models.CharField(max_length=255)
    content = models.TextField()

    def __unicode__(self):
        return self.title

Corresponding Tastypie Resource

# api.py
"""
Example REST API
"""
from models import Note
from tastypie.resources import ModelResource
from tastypie.authorization import DjangoAuthorization
from tastypie.authentication import BasicAuthentication

class NoteResource(ModelResource):
    class Meta:
        resource_name = 'notes'
        queryset = Note.objects.all()
        authentication = BasicAuthentication()
        authorization = DjangoAuthorization()

The above code creates an api using the provided resource_name. Tastypie comes with built in support for HTTP Basic Authentication (never use basic without SSL!), and Django Authorization. This allows users to include HTTP Basic Authentication credentials with every request, so that all the state needed is included in the request (rather than relying on a session). The DjangoAuthorization included with Tastypie uses the built in permissions and authorization framework built into Django to authorize operations on objects. Tastypie is careful to return the proper HTTPstatus codes if authentication or authorization fails (and other conditions too).

The ModelResource class that NoteResource inherits from is a Django model specific subclass of the Resource class mentioned above. It automatically maps HTTP GET, PUT, PATCH, and DELETE requests to the appropriate Django ORM operations.

Include Tastypie URLs

# urls.py
from django.conf.urls.defaults import *
from .api import NoteResource

note_resource_api = NoteResource(api_name='v1')

urlpatterns = patterns('',
    (r'^api/', include(note_resource_api.urls)),
)

The above code wires up the NoteResource API URLs. By default, Tastypie includes URLs for listing resources end points in the API, listing resources (with pagination) for each resource, and the schema for each resource. I have included the 'v1' argument as a version number to the API.

Using the API

We can consume the REST API from any HTTP client. For this example I use python-requests.

>>>from requests.auth import HTTPBasicAuth
>>>auth = HTTPBasicAuth(username, password)
>>>response = requests.get('https://server/api/v1/', auth=auth)
>>>response.json()
{u'notes': {u'list_endpoint': u'/api/v1/notes/', u'schema': u'/api/v1/notes/schema/'}}

The URI /api/v1 is a top level view of the API, and from it we can discover the URIs to all resources included in it. We can look at the schema for a resource to see how it is structured.

>>>response = requests.get('https://server/api/v1/notes/schema/', auth=auth)
>>>import pprint
>>>pprint.pprint(response.json())
{u'allowed_detail_http_methods': [u'get',
                              u'post',
                              u'put',
                              u'delete',
                              u'patch'],
 u'allowed_list_http_methods': [u'get', u'post', u'put', u'delete', u'patch'],
 u'default_format': u'application/json',
 u'default_limit': 20,
 u'fields': {u'content': {u'blank': False,
                      u'default': u'',
                      u'help_text': u'Unicode string data. Ex: "Hello World"',
                      u'nullable': False,
                      u'readonly': False,
                      u'type': u'string',
                      u'unique': False},
         u'id': {u'blank': True,
                 u'default': u'',
                 u'help_text': u'Integer data. Ex: 2673',
                 u'nullable': False,
                 u'readonly': False,
                 u'type': u'integer',
                 u'unique': True},
         u'resource_uri': {u'blank': False,
                           u'default': u'No default provided.',
                           u'help_text': u'Unicode string data. Ex: "Hello World"',
                           u'nullable': False,
                           u'readonly': True,
                           u'type': u'string',
                           u'unique': False},
         u'title': {u'blank': False,
                    u'default': u'No default provided.',
                    u'help_text': u'Unicode string data. Ex: "Hello World"',
                    u'nullable': False,
                    u'readonly': False,
                    u'type': u'string',
                    u'unique': False}}}

This response tells us quite a lot about the API for this particular resource. The first two entries tell which methods are allowed for list and detail views (a list of object vs an individual object). The methods for NoteResource are defaults which we could easily override. The default_limit tells us how many objects will be returned per page. The fields data contains the name and detailed attributes of each object field.

>>>import json
>>>note = {"title": "foo-title", "content": "bar-content"}
>>>response = requests.post('https://server/api/v1/notes/', data=json.dumps(note), auth=auth, headers={"content-type": "application/json"})
<Response [201]>

The above code is all that is needed to create a note! Now when we query the API we can retrieve it.

>>>response = requests.get('http://server/api/v1/notes/', auth=auth)
>>>pprint.pprint(response.json())
{u'meta': {u'limit': 20,
       u'next': None,
       u'offset': 0,
       u'previous': None,
       u'total_count': 1},
 u'objects': [{u'content': u'bar-content',
           u'id': 1,
           u'resource_uri': u'/api/v1/notes/1/',
           u'title': u'foo-title'}]}

The response contains two top level objects. The meta object contains the total number of objects, the offset to the current page, and URIs to the next and previous pages if they exist. The objects object contains the list of objects returned for the current page. The API also accepts pagination arguments.

>>># get the first 50 objects
>>>response = requests.get('http://server/api/v1/notes/?limit=50', auth=auth)
>>># get objects 10 through 20
>>>response = requests.get('http://server/api/v1/notes/?offset=10&limit=10', auth=auth)

You can also filter objects with the API (it must be enabled for the resource) using Django ORM-like filters.

>>># get up to 10 objects whose title begins with bar
>>>response = requests.get('http://server/api/v1/notes/?title__startswith=bar&limit=10', auth=auth)
>>># get up to 5 objects whose title contains with foo
>>>response = requests.get('http://server/api/v1/notes/?title__contains=bar&limit=10', auth=auth)

Do you need to validate user input? Just add a validation attribute to your resource.

# api.py
"""
Example REST API
"""
from models import Note
from tastypie.resources import ModelResource
from tastypie.authorization import DjangoAuthorization
from tastypie.authentication import BasicAuthentication
from tastypie.validaton import Validation


class NoteValidation(Validation):
"""
Make sure title and content are not empty
"""
    def is_valid(self, bundle, request=None):
        if not bundle.data:
            return {'__all__': 'No data provided.'}
        errors = {}
        if bundle.data.get('title', '') == '':
            errors['title'] = 'Title cannot be empty'
        if bundle.data.get('content', '') == '':
            errors['content'] = 'Content cannot be empty'
        return errors


class NoteResource(ModelResource):
    class Meta:
        resource_name = 'notes'
        queryset = Note.objects.all()
        authentication = BasicAuthentication()
        authorization = DjangoAuthorization()
        validation = NoteValidation()

Non ORM Resources

Tastypie can also work with non ORM resources. All you need to do is provide a subclass that implements the methods that Tastypie needs for creating, updating, and deleting objects.Below is a stub of such a subclass with the method documentation strings explaining what each needs to do.

# The following methods will need overriding regardless of your
# data source.
def detail_uri_kwargs(self, bundle_or_obj):
    """
    This method needs to return a dictionary with a pk (primary key)
    that can be used to uniquely identify the object.
    """
    kwargs = {}
    kwargs['pk'] = magic_pk_method(bundle_or_obj)
    return kwargs

def get_object_list(self, request):
    """
    This method simply returns the list of objects, unfiltered
    """
    results = get_my_objects()
    return results

def obj_get_list(self, request=None, **kwargs):
    """
    This method returns the list of objects, with any user provided filters applied
    """
    return filtered_objects()

def obj_get(self, request=None, **kwargs):
    """
    This method returns an individual object from a PK
    """
    obj = my_get_obj(kwargs['pk'])
    return obj

def obj_create(self, bundle, request=None, **kwargs):
    """
    This method creates an object from the arguments and
    stores it on the bundle
    """
    bundle.obj = MyObject(kwargs)
    bundle = self.full_hydrate(bundle)
    return bundle

def obj_update(self, bundle, request=None, **kwargs):
    """
    This method updates or creates an object
    """
    return self.obj_create(bundle, request, **kwargs)

def obj_delete_list(self, request=None, **kwargs):
    """
    This method should delete a list of objects
    """

def obj_delete(self, request=None, **kwargs):
    """
    This method should delete an individual object
    """

Multiple Authentication Support

Tastypie supports multiple authentication schemes. Suppose that you want to support HTTP Basic Authentication to support API users and Session authentication for JavaScriptclients with Django Sessions. Here is how you construct your resource.

# api.py
"""
Example REST API
"""
from models import Note
from tastypie.resources import ModelResource
from tastypie.authorization import DjangoAuthorization
from tastypie.authentication import MultiAuthentication, BasicAuthentication, SessionAuthentication

class NoteResource(ModelResource):
    class Meta:
        resource_name = 'notes'
        queryset = Note.objects.all()
        authentication = MultiAuthentication(BasicAuthentication(), SessionAuthenticaton())
        authorization = DjangoAuthorization()

Testing

Testing with Tastypie is really straightforward. In fact, it's author firmly believes in test driven development. Tastypie comes with a full featured test client for interacting with the API in test cases. It would be superflous to add code here, as the official documentation has a full code example: link.

Conclusion

There are many features of Tastypie that I didn't discuss here, so you should read the docs if you want to know more!