-
Building a website API with Django. Part 1: API functions
Aug. 11, 2008 at 06:59:02 CESTWAPI and django-oauthsp are reaching the point when they are starting to become useful, so I decided it was time to write some articles explaining how they work and how you can use them in your sites. I'm currently using both of them in production at byNotes and I haven't run into any problems for now, but YMMV.
This article talks about the principles behind WAPI and walks you trough the creation of a simple API function. Next articles in this series will explain more advanced concepts like custom serializations, user authentication against django.contrib.auth or using OAuth.
Design
Let's start by talking about the design ideas behind WAPI. My main motivation when I started writing it was abstracting most of the details involved in publishing a web API, since almost 100% of sites have the same requirements: ReSTful resources (sometimes SOAP or XML-RPC), serialization in different formats and user authentication. Handling all of this as views is not difficult, but it's tedious and a lot of times requires more code than the API itself. So, why not let the developer focus on providing useful endpoints and let a framework do the rest?
WAPI only requires you to write a class. That's it. Every method of the class translates to an API method and all you have to is return the objects you want to be serialized and WAPI takes care of serializing them in the appropiate format. Isn't that beautiful? It even will validate the function parameters for you and take care of user authentication. Do you want more? Well, there's something more: WAPI will also autodocument your API, so, as I said before, all you have to do is write a Python class.
The code
Let's move to the code. For this tutorial, I'm going to use this model:
class Event(models.Model): place_name = models.CharField(max_length=50) what = models.CharField(max_length=50) when = models.DatetimeField()
We're going to use the predefined serialization, which includes all the fields defined in the model, so we don't need to talk about wapi.serializers for now.
A function with no parameters
So, it's time to write the API. We're exposing a simple function, list all the events happening today.
from wapi.responses import SerializableResponse class EventApi(object): def today(self, request, dct): d = date.today() return SerializableResponse(Event.objects.filter(when__year=d.year, when__month=d.month, when__day=d.day)
SerializableResponse is a special response, which takes a queryset and, optionally, serialization parameters. WAPI knows how to translate SerializableResponses to the required transport and the format, so you don't have to worry about that.
Now it's time to plug your API into your urls.py. WAPI uses a binding for doing this, so all you have to do is tell WAPI which binding do you want and the base url for it.
from wapi.bindings import RestBinding from mysite.api import EventsApi urlpatterns = patterns('mysite.myapp.views', ... ... (r'^api/1.0/rest/%s$' % RestBinding.PATTERN, RestBinding(api=EventsApi())), ... )
As you can see, I specified the transport method I'm using in the URL for the API. Currently WAPI only works over ReST, but it should support XML-RPC and SOAP soon.
If you point your browser to http://example.com/api/1.0/rest/today.json you'll get something like this:
[ { id: 7, place_name: "Foo Place", what: "Writing a tutorial", when: "2008-08-11 17:21:56" }]
Or, if you prefer, you could also open http://example.com/api/1.0/rest/today.xml or http://example.com/api/1.0/rest/today.yaml, WAPI also supports them.
A function with parameters
So, let's suppose you want to let your API consumers request the details for an event given it's id. Let's start with the basic bits:
from wapi.responses import SerializableResponse class EventApi(object): ... more methods ... def event(self, request, dct): return SerializableResponse(Event.objects.filter(id=dct['id'])
As you see, WAPI functions receive all their parameters inside a dict. This function is pretty staight-forward, bit it has some problems. For example: what happens when the event doesn't exist? or when the user omits the id parameter? I'm sure you know it, but for the sake of completeness, you'll get a 500 error. Not very user (or developer) friendly.
Parameter validation
One of the features I previously mentioned was parameter validation, so let's use it.
from wapi.responses import SerializableResponse from wapi.decorators import required_parameter from django.utils.translation import ugettext_lazy as _ class EventApi(object): ... more methods ... @required_parameter('id', int, _('The id of the event')) def event(self, request, dct): return SerializableResponse(Event.objects.filter(id=dct['id'])
So we're covered the cases when the parameter is absent or it's not an int. As a bonus, dct['id'] will now be a int instead of an unicode string (which is usefull sometimes).
Serializing single objects
SerializableResponse is meant to serialize lists (or any iterable) of objects, so the response to this method will always be a list. That's not very pretty when you're always returning a single object, so let's change it to return a SingleSerializableResponse:
from wapi.responses import SingleSerializableResponse from wapi.decorators import required_parameter from django.utils.translation import ugettext_lazy as _ class EventApi(object): ... more methods ... @required_parameter('id', int, _('The id of the event')) def event(self, request, dct): return SingleSerializableResponse(Event.objects.get(id=dct['id'])
SingleSerializableResponse works in the same way as SerializableResponse, however the latter works on iterables while the former works on a single object.
Returning an empty response
We still have a problem to solve. If the event does not exist, we are still returning a 500 error. There are basically two approaches here: some people prefer to return a 404 error, while others say that an empty 200 response is better. Whatever you prefer, WAPI can do it. If you want to return a 404 error, just use the django shortcut get_object_or_404:
from wapi.responses import SingleSerializableResponse from wapi.decorators import required_parameter from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 class EventApi(object): ... more methods ... @required_parameter('id', int, _('The id of the event')) def event(self, request, dct): return SingleSerializableResponse(get_object_or_404(Event, id=dct['id']))
However, if you prefer an empty response, just use the get_object_or_empty WAPI shortcut:
from wapi.responses import SingleSerializableResponse from wapi.decorators import required_parameter from wapi.shortcuts import get_object_or_empty from django.utils.translation import ugettext_lazy as _ class EventApi(object): ... more methods ... @required_parameter('id', int, _('The id of the event')) def event(self, request, dct): return SingleSerializableResponse(get_object_or_empty(Event, id=dct['id']))
Next article in this series
There are some more topics about WAPI and django-authsp I want to cover, but I'm not sure about what's the best order, so if you liked this article leave a comment and tell me what you want to read next:
- More complex functions: namespaces, optional parameters and allowed request methods.
- Complex object serializations: chaining, including, conditional serializations, serializations with parameters.
- Authentication: Cookie, HTTP Basic, HTTP Digest and OAuth.
- API documentation: How to tell the world about your API.
Great post as introduction to wapi, Thanks!
I would like to read more about Authentication and API documentation. +1 for them.
I try your example but I can't find wapi.bindings and RestBinding in wapi.zip. Am I missing something?
Hi Obje Zure,
The code I'm documenting does not match the last release. I'm currently preparing the next release, but it will take a few days.
Great article! I spotted two inaccuracies though:
You import "required_parameter" but use the decorator "required_parameters". Which one is correct?
Your "get_object_or_404" call is missing a reference to the Event class as its first argument.
(also, the comment form on this page didn't validate without checking the e-mail notify checkbox)
Hi Antti,
It's required_parameter, I mistyped it the first time and then I copied and pasted. I've fixed it, as well as the error in get_object_or_404 and the issue with the form. Thanks for spotting them :).
Gr8 article and series, will try this stuff on my new project
A few typos:
You define the class as: class EventApi(object):"
But you import it as (plural): from mysite.api import EventsApi
Missing closing param in the method today():
return SerializableResponse(Event.objects.filter(when__year=d.year, when__month=d.month, when__day=d.day)
Missing closing param in the method event():
return SerializableResponse(Event.objects.filter(id=dct['id'])
return SingleSerializableResponse(Event.objects.get(id=dct['id'])
How do I use more complex urls that include parameters?
For example if I want
/api/1.0/rest/profile/{username}.{format}
to allow {username} to be extracted as a parameter and passed to the method.
One possible way might be for me to derive from RestBinding, and then add an additional constructor parameter.
Is there a cleaner way to do so?