ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Using REST with Ajax

by Nic Ferrier
02/23/2006

This article shows how to use Ajax techniques to make web apps with REST APIs.

Everyone's talking about REST these days. Lots of people are still struggling with it, and there's good reason for that--REST is actually quite difficult to fit into the browser-based HTML Web, for two reasons:

  1. Current HTML forms support only GET and POST, not PUT or DELETE
  2. HTML forms always involve a page change

As far as I'm concerned, this is fair enough; REST is primarily a web services platform, an alternative to CORBA, SOAP, and XMLRPC, not a user interface. Still, at least some of the time, it's nice to use a REST API as the foundation for a web app. On the other hand, Ajax can be really complicated, and it always seems to involve lots of different bits of code.

That is the point of this article. REST's clean definition of an application's architecture solves at least some of the problems with Ajax; and using Ajax to a REST web app leaves the REST API uncomplicated and pure.

REST API

The best way for me to explain this is to refer to a real REST API. I recently built a multiuser ATOM/RSS aggregator. Each user can have a list of feeds (URLs, basically) and manipulate those list items. I built a simple REST API for managing the lists.

The feed aggregator needs to:

Clearly I could model the list of a user's feeds as a resource:

/feed

... and each feed as a subresource of that:

/feed/106

That is the great thing about REST: it's easy to work out the details of a pure REST implementation:

This is all very simple, and simplicity is of course why REST is so powerful.

This code is pretty easy to do with just about any web server and language combination. I used mod_python.

Here's the main handler and GET method implementation:

import os
import re
import urllib
 
from mod_python import apache
from mod_python import util
 
def handler(http):
    if http.method == "GET":
        return get(http)
    elif http.method == "POST":
        return post(http)
    elif http.method == "DELETE":
        return delete(http)
    return apache.HTTP_NOT_ACCEPTABLE
 
def get(http):
    """Display a list of feeds."""
    username = http.user
    form = util.FieldStorage(http)
 
    def feeds():
        try:
            dbcon = PgSQL.connect(http.get_options()["DBURL-" + http.hostname])
            curs = dbcon.cursor()
            curs.execute("select id, url from get_feeds('%s')" % (username))
            result = curs.fetchone()
            while result != None:
                yield result
                result = curs.fetchone()
            dbcon.close()
        except:
            pass
        return

    http.content_type = "text/html"
    xmlout(feeds, http)
    return apache.OK

def xmlout(generator, out):
    """Use 'generator' to get the data and send it to 'out'"""
    print >>out, "<?xml version="1.0"?>"
    print >>out, "<?xsl-stylesheet href='/display-feed.xslt' version="1.0"?>"
    print >>out, "<feeds>"
    for id,url in generator():
      print >>out,
      "<feed><id>%s</id><url>%s</url></feed>" % (id, re.sub("&", "&amp;", url))
    print >>out, "</feeds>"

The GET returns an HTML page of the user's feeds. Notice that there is an XML representation in the code, so I could add content negotiation to the mix to support both XML and HTML output.

Here's the POST implementation. This has to add a new feed to the user's list of feeds:

def post(http):
    """Update a user's list of feeds with a new feed."""
    username       = http.user
    form           = util.FieldStorage(http)
    suggested_feed = form["suggested_feed"]

    def add_feeds():
        try:
            dbcon = PgSQL.connect(http.get_options()["DBURL-" + http.hostname])
            curs  = dbcon.cursor()
            curs.execute("select add_feed('%s', '%s')" % (username, suggested_feed))
            curs.execute("select id, url from get_feeds('%s')" % (username))
            result = curs.fetchone()
            while result != None:
                yield result
                result = curs.fetchone()
            dbcon.close()
        except:
            pass
        return

    http.content_type = "text/html"
    http.status = HTTP_CREATED
    xmlout(add_feeds, http)
    return apache.OK

Here's what the HTTP spec says about POST:

The POST method is used to request that the web server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI.

The spec goes on to say that one specific reason for POST is to allow append operations on databases. That is exactly what this part of the feeds application does; it appends a new feed to a user's list.

I am cheating a bit here. The return from the POST is not exactly REST compliant. A 201 response indicates that the server has created a new resource, but this response is supposed to include a resource indicating a reference to the new resource (including a Location header, as in a redirect response).

However, the example simply returns the same list of feeds as returned from the GET method. In my defense, this isn't too big a heresy, and it simplifies these examples a bit. Again, it's possible to support XML output by adding content negotiation.

The last method to implement is the DELETE method. This must remove a feed specified by its ID from the user's feed list:

def delete(http):
    """Remove a feed from the user's list"""
    username = http.user
    todelete_match = re.match("/([0-9]+)", http.path_info)
    todelete = todelete_match.groups()[0]

    try:
        dbcon = PgSQL.connect(http.get_options()["DBURL-" + http.hostname])
        curs = dbcon.cursor()
        curs.execute("select delete_user_feed('%s', %d)" % (username, int(todelete)))
    except Exception, e:
        # Possibly just from the proc, whatever - it failed.
        http.status = apache.HTTP_NOT_FOUND
        dbcon.close()
    else:
        dbcon.commit()
        dbcon.close()
        http.status = apache.HTTP_OK

    return apache.OK

This is pretty obvious. It simply makes another database call to remove the specified feed.

Building a Web App on Top of the API

This is a pretty simple API, yet it models most of the things you need to do with a database of anything.

The trouble is, how do you get this into a browser? The GET is easy; a user can GET the list of feeds and maybe keep a bookmark to it so he can easily retrieve the list.

POST is also quite easy. I could have a page with a text box on it so that a user can submit a URL. In fact, I have a Greasemonkey script to find RSS or ATOM feeds in a page and ask the user to POST it to the feeds web app.

DELETE is the hard option to support, because browser web forms don't allow you to send a DELETE operation. This screws up the whole app. How can you allow users to remove feeds from their lists?

One way around this is to make a POST with a method identifier, so you have HTML like:

<form method="POST" action="/someuri">
<input type="hidden" name="method" value="DELETE"/>
</form>

This is all very good, but the trouble is you need to add specific code to your web app for handling this. It's just another thing to get wrong, and it still doesn't entirely fix the problem--submitting the form will always cause a context change. In other words, the server returns a page from the POST and the browser displays it.

There are ways around that, but they're all clunky. The most common is to return a redirect to the page from which the user submitted the pseudo-delete. That's more complicated code in the handler.

Here's Ajax

Here's where Ajax comes in. Use the XMLHttpRequest object of Ajax to send the DELETE to the server.

How do you do this? The list of feeds is the natural place to start. If the HTML version of the list of feeds allowed feeds to be deleted, then you could add some Ajax code to that page to perform deletes.

So as not to complicate things by adding XSLT, here is some straight HTML that the GET method might deliver:

<html>
<head>
<script>
<![CDATA[
// Delete any marked feeds
function delFeeds ()
{
  var feeds = document.getElementById("feeds");
  for (var i = 0; i < feeds.elements.length; i++)
   {
      if (feed.elements[i].checked)
      {
         var feed   = feeds.elements[i];
         var req    = new XMLHttpRequest();
         req.feedId = feed.id;

        function delHandler(evt)
        {
           if (evt.target.readyState == 4 && evt.target.status == 200)
           {
             var feedLabel = document.getElementById("feed_" + evt.target.feedId).style.display = "none";
             feed.checked = false;
           }
         }

         req.onload = delHandler;
         req.open("DELETE", "/feeds/" + req.feedId, true, "nic", "hello");
         req.send("");
       }
    }
}

]]>
</script>
</head>
<body>
<h1>Current feeds</h1>
<div id="feed_form">
    <form id="feeds" action="javascript:;">
        <label>Delete?</label>
        <label id="feed_1">
           <input type="checkbox" id="1" name="1" value="1"/>
           http://www.tapsellferrier.co.uk/nics-blog.xml
        </label>
        <label id="feed_2">
           <input type="checkbox" id="2" name="2" value="2"/>
           http://news.bbc.co.uk/rss/newsonline_uk_edition/technology/rss.xml
        </label>
        <label id="feed_3">
           <input type="checkbox" id="3" name="3" value="3"/>
           http://newsrss.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss.xml
        </label>
        <button name="delete" onclick="delFeeds();">delete</button>
    </form>
</div>
</body>
</html>

This is an HTML form of three items, each with a check box. The idea is to allow a user to select all the items to be deleted and then press the delete button. It doesn't have to be this way, of course--you could have an onclick handler on each feed item to call the Ajax, but I prefer it this way.

Now it's pretty clear. The JavaScript sends the correct HTTP request to the REST API with simple DOM code.

There is one more point of interest here: synchronizing the user's view of the feeds list with the actual state of the feeds list on the back end. When a user deletes any feed, this view should update to reflect the deletion. How can you make that happen? The Ajax could easily cause a page refresh, and that would cause the display of the feed list to reflect the state in the database.

The trouble with a refresh is that it involves another network trip. For a simple edit, this is unnecessary. The browser knows what the user has removed. The code actually turns off the display of the item being deleted.

And Finally ...

I think that this is the right way to go with web apps. Ajax can be complicated, but REST makes it simpler. In turn, Ajax simplifies REST apps--or at least reduces the need for complicating them. By combining them, you get the best of both worlds.

Nic Ferrier is an independent software consultant specializing in web applications.


Return to the Python DevCenter.

Copyright © 2009 O'Reilly Media, Inc.