Building Better Web Services With Django (Part 2)

In the first part I talked about using the Content-Type and Accept HTTP headers to allow a single website to be use both by humans and programs.n In the previous part I gave a decorator which can be used to make working with JSON very easy. For our use though this isn’t great because a view decorated in this way only accepts JSON as the POST body and only returns JSON, regardless of the HTTP headers.

The decorator given below relies on a django snippet to decode the Accept header for us so don’t forget to added it to your middleware.

def content_type(func, common=None, json_in=None, json_out=None, form_in=None):
    def wrapper(req, *args, **kwargs):
        # run the common function, if we have one
        if common is not None:
            args, kwargs = common(req, *args, *kwargs), {}
            if isinstance(args, HttpResponse): return args
        content_type = req.META.get("content_type", "")
        if content_type == "application/json":
            args, kwargs = json_in(req, json.loads(req.raw_post_data), *args, *kwargs), {}
        elif content_type == "application/x-www-form-urlencoded":
            args, kwargs = json_in(req, req.POST, *args, *kwargs), {}
        else:
            return HttpResponse(status=415, "Unsupported Media Type")

        if isinstance(args, HttpResponse): return args

        for (media_type, q_value) in req.accepted_types:
            if media_type == "text/html":
                return func(req, args, kwargs)
            else:
                r = json_out(req, args, kwargs)
                if isinstance(r, HttpResponse):
                    return r
                else:
                    return HttpResponse(json.dumps(r), mimetype="application/json")
         return func(req, args, kwargs)
    return wrapper

So, how can we use this decorator? Let’s imagine we’re creating a blog and we have a view which displays a post on that blog. If they user posts it should create a new comment. Firstly we create a function, common, which gets the blog object and returns a 404 if it doesn’t exist. The return of this function is passed onto all other functions as their arguments.

def common(req, blog_id):
    try:
        return (get_post_by_id(int(blog_id)), )
    except ValueError:
        return HttpResponse(status=404)

Next we write two functions to handle the cases where the users POSTs a form encoded body, or some JSON. The return values of these functions are passed onto the chosen output function as the arguments.

def json_in(req, json, blog_post):
    # process json
    return (blog_post ,)

def form_in(req, form, blog_post):
    # process form
    return (blog_post, )

The JSON output function doesn’t need to return an HttpResponse object like a normal Django view because the output is automatically encoded as a string and wrapped in a response object.n

def json_out(req, blog_post):
    return blog_post.to_json()

Finally we come to the HTML output function. This function is also called if not mime type in Accept is suitable.

@content_type(common=common, json_in=json_in, json_out=json_out, form_in=form_in)
def blog_post(req, blog_post):
    return render_to_template("post.html", {"post": blog_post})

This decorator is really little more than a sketch. Many more content types could be supported, but hopefully it gives a good example of how you can write a very flexible webservice and still reduce code duplication as much as possible.

Want to read more like this? Follow me with your favourite feed reader (e.g. Feedly), or subscribe to my SubStack newsletter.

Comments

Thanks for great code and doc. Kudos!

Benny Daon

25 Apr 2009

very nice, but could you please change the ugly and eye-hurting colors? The light blue background makes especially the code nearly unreadable. Also maybe you could enhance yuor blog a lot by using syntax highliting for the code parts - thanks!!!

Galer Organ Bestofio

16 Apr 2010