Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.

Channels ▼

Web Development

Building RESTful APIs with Tornado

Tornado is a Python Web framework and asynchronous networking library that provides excellent scalability due to its non-blocking network I/O. It also greatly facilitates building a RESTful API quickly. These features are central to Tornado, as it is the open-source version of FriendFeed's Web server. A few weeks ago, Tornado 3.  was released, and it introduced many improvements. In this article, I show how to build a RESTful API with the latest Tornado Web framework and I illustrate how to take advantage of its asynchronous features.

Mapping URL Patterns to Request Handlers

To get going, download the latest stable version and perform a manual installation or execute an automatic installation with pip by running pip install tornado.

To build a RESTful API with Tornado, it is necessary to map URL patterns to your subclasses of tornado.web.RequestHandler, which override the methods to handle HTTP requests to the URL. For example, if you want to handle an HTTP GET request with a synchronous operation, you must create a new subclass of tornado.web.RequestHandler and define the get() method. Then, you map the URL pattern in tornado.web.Application.

Listing One shows a very simple RESTful API that declares two subclasses of tornado.web.RequestHandler that define the get method: VersionHandler and GetGameByIdHandler.

Listing One: A simple RESTful API in Tornado.

from datetime import date
import tornado.escape
import tornado.ioloop
import tornado.web

class VersionHandler(tornado.web.RequestHandler):
    def get(self):
        response = { 'version': '3.5.1',
                     'last_build':  date.today().isoformat() }

class GetGameByIdHandler(tornado.web.RequestHandler):
    def get(self, id):
        response = { 'id': int(id),
                     'name': 'Crazy Game',
                     'release_date': date.today().isoformat() }

application = tornado.web.Application([
    (r"/getgamebyid/([0-9]+)", GetGameByIdHandler),
    (r"/version", VersionHandler)

if __name__ == "__main__":

The code is easy to understand. It creates an instance of tornado.web.Application named application with the collection of request handlers that make up the Web application. The code passes a list of tuples to the Application constructor. The list is composed of a regular expression (regexp) and a tornado.web.RequestHandler subclass (request_class). The application.listen method builds an HTTP server for the application with the defined rules on the specified port. In this case, the code uses the default 8888 port. Then, the call to tornado.ioloop.IOLoop.instance().start() starts the server created with application.listen.

When the Web application receives a request, Tornado iterates over that list and creates an instance of the first tornado.web.RequestHandler subclass whose associated regular expression matches the request path, and then calls the head(), get(), post(), delete(), patch(), put() or options() method with the corresponding parameters for the new instance based on the HTTP request. For example, Table 1 shows some HTTP requests that match the regular expressions defined in the previous code.

HTTP verb and request URL Tuple (regexp, request_class) that matches the request path RequestHandler subclass and method that is called

GET http://localhost:8888/getgamebyid/500

(r"/getgamebyid/([0-9]+)", GetGameByIdHandler)


GET http://localhost:8888/version

(r"/version", VersionHandler)


Table 1: Matching HTTP requests.

The simplest case is the VersionHandler.get method, which just receives self as a parameter because the URL pattern doesn't include any parameter. The method creates a response dictionary, then calls the self.write method with response as a parameter. The self.write method writes the received chunk to the output buffer. Because the chunk (response) is a dictionary, self.write writes it as JSON and sets the Content-Type of the response to application/json. The following lines show the example response for GET http://localhost:8888/version and the response headers:

{"last_build": "2013-08-08", "version": "3.5.1"
Date: Thu, 08 Aug 2013 19:45:04 GMT
Etag: "d733ae69693feb59f735e29bc6b93770afe1684f"
Content-Type: application/json; charset=UTF-8
Server: TornadoServer/3.1
Content-Length: 48</p>

If you want to send the data with a different Content-Type, you can call the self.set_header with "Content-Type" as the response header name and the desired value for it. You have to call self.set_header after calling self.write, as shown in Listing Two. It sets the Content-Type to text/plain instead of the default application/json in a new version of the VersionHandler class. Tornado encodes all header values as UTF-8.

Listing Two: Changing the content type.

class VersionHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Version: 3.5.1. Last build: " + date.today().isoformat())
        self.set_header("Content-Type", "text/plain")

The following lines show the example response for GET http://localhost:8888/version and the response headers with the new version of the VersionHandler class:

Server: TornadoServer/3.1
Content-Type: text/plain
Etag: "c305b564aa650a7d5ae34901e278664d2dc81f37"
Content-Length: 38
Date: Fri, 09 Aug 2013 02:50:48 GMT

The GetGameByIdHandler.get method receives two parameters: self and id. The method creates a response dictionary that includes the integer value received for the id parameter, then calls the self.write method with response as a parameter. The sample doesn't include any validation for the id parameter in order to keep the code as simple as possible, as I'm focused on the way in which the get method works. I assume you already know how to perform validations in Python. The following lines show the example response for GET http://localhost:8888/getgamebyid/500 and the response headers:

{"release_date": "2013-08-09", "id": 500, "name": "Crazy Game"}

Content-Length: 63
Server: TornadoServer/3.1
Content-Type: application/json; charset=UTF-8
Etag: "489191987742a29dd10c9c8e90c085bd07a22f0e"
Date: Fri, 09 Aug 2013 03:17:34 GMT

If you need to access additional request parameters such as the headers and body data, you can access them through self.request. This variable is a tornado.httpserver.HTTPRequest instance that provides all the information about the HTTP request. The HTTPRequest class is defined in httpserver.py.

Related Reading

More Insights

Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.