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 backinto 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 supportfor HTTP Basic Authentication (never use basic without SSL!), and Django Authorization. This allows usersto 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 entriestell 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
tellsus 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 paginationarguments.
>>># 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 subclassthat 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 supportHTTP Basic Authentication to support API users and Session authentication for JavaScript clients 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!