REST APIs have become a common way to establish an interface between web back-ends and front-ends, and between different web services. The simplicity of this kind of interface, and the ubiquitous support of the HTTP and HTTPS protocols across different networks and frameworks, makes it an easy choice when considering interoperability issues.

Bottle is a minimalist Python web framework. It is lightweight, fast, and easy to use, and is well-suited to building RESTful services. A bare-bones comparison made by Andriy Kornatskyy put it among the top three frameworks in terms of response time and throughput (requests per second). In my own tests on the virtual servers available from DigitalOcean, I found that the combination of the uWSGI server stack and Bottle could achieve as low as a 140μs overhead per request.

In this article, I’ll provide a walkthrough of how to build a RESTful API service using Bottle.

Installation and Configuration

The Bottle framework achieves its impressive performance in part thanks to its light weight. In fact the entire library is distributed as a one-file module. This means that it does not hold your hand as much as other frameworks, but it is also more flexible and can be adapted to fit into many different tech stacks. Bottle is therefore best suited for projects where performance and customizability are at a premium, and where the time-saving advantages of more heavy-duty frameworks are less of a consideration.

The flexibility of Bottle makes an in-depth description of setting up the platform a bit futile, since it may not reflect your own stack. However, a quick overview of the options, and where to learn more about how to set them up, is appropriate here:

Installation

Installing Bottle is as easy as installing any other Python package. Your options are:

Install on your system using the system’s package manager. Debian Jessie (current stable) packages the version 0.12 as python-bottle.

Install on your system using the Python Package Index with pip install bottle .

. Install on a virtual environment (recommended).

To install Bottle on a virtual environment, you’ll need the virtualenv and pip tools. To install them, please refer to the virtualenv and pip documentation, though you probably have them on your system already.

In Bash, create an environment with Python 3:

$ virtualenv -p `which python3` env

Suppressing the -p `which python3` parameter will lead to the installation of the default Python interpreter present on the system – usually Python 2.7. Python 2.7 is supported, but this tutorial assumes Python 3.4.

Now activate the environment and install Bottle:

$ . env/bin/activate $ pip install bottle

That’s it. Bottle is installed and ready to use. If you’re not familiar with virtualenv or pip, their documentation is top notch. Take a look! They are well worth it.

Server

Bottle complies with Python’s standard Web Server Gateway Interface (WSGI), meaning it can be used with any WSGI-compliant server. This includes uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine, and others.

The correct way to set it up varies slightly with each environment. Bottle exposes an object that conforms to the WSGI interface, and the server must be configured to interact with this object.

To learn more about how to set up your server, refer to the server’s docs, and to Bottle’s docs, here.

Database

Bottle is database-agnostic and doesn’t care where the data is coming from. If you’d like to use a database in your app, the Python Package Index has several interesting options, like SQLAlchemy, PyMongo, MongoEngine, CouchDB and Boto for DynamoDB. You only need the appropriate adapter to get it working with the database of your choice.

Bottle Framework Basics

Now, let’s see how to make a basic app in Bottle. For code examples, I will assume Python >= 3.4. However, most of what I’ll write here will work on Python 2.7 as well.

A basic app in Bottle looks like this:

import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

When I say basic, I mean this program doesn’t even “Hello World” you. (When was the last time you accessed a REST interface that answered “Hello World?”) All HTTP requests to 127.0.0.1:8000 will recieve a 404 Not Found response status.

Apps in Bottle

Bottle may have several instances of apps created, but for the sake of convenience the first instance is created for you; that’s the default app. Bottle keeps these instances in a stack internal to the module. Whenever you do something with Bottle (such as running the app or attaching a route) and don’t specify which app you’re talking about, it refers to the default app. In fact, the app = application = bottle.default_app() line doesn’t even need to exist in this basic app, but it is there so that we can easily invoke the default app with Gunicorn, uWSGI or some generic WSGI server.

The possibility of multiple apps may seem confusing at first, but they add flexibility to Bottle. For different modules of your application, you might create specialized Bottle apps by instantiating other Bottle classes and setting them up with different configurations as needed. These different apps could be accessed by different URLs, through Bottle’s URL router. We won’t delve into that in this tutorial, but you are encouraged to take a took at Bottle’s documentation here and here.

Server Invocation

The last line of the script runs Bottle using the indicated server. If no server is indicated, as is the case here, the default server is Python’s built-in WSGI reference server, which is only suitable for development purposes. A different server can be used like this:

bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)

This is syntactic sugar that let’s you start the app by running this script. For example, if this file is named main.py , you can simply run python main.py to start the app. Bottle carries quite an extensive list of server adapters that can be used this way.

Some WSGI servers don’t have Bottle adapters. These can be started with the server’s own run commands. On uWSGI, for instance, all you’d have to do would be to call uwsgi like this:

$ uwsgi --http :8000 --wsgi-file main.py

A Note on File Structure

Bottle leaves your app’s file structure entirely up to you. I’ve found my file structure policies evolve from project to project, but tend to be based on an MVC philosophy.

Building Your REST API

Of course, nobody needs a server that only returns 404 for every requested URI. I’ve promised you we’d build a REST API, so let’s do it.

Suppose you’d like to build an interface that manipulates a set of names. In a real app you would probably use a database for this, but for this example we will just use the in-memory set data structure.

The skeleton of our API might look like this. You can place this code anywhere in the project, but my recommendation would be a separate API file, such as api/names.py .

from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass

Routing

As we can see, routing in Bottle is done using decorators. The imported decorators post , get , put , and delete register handlers for these four actions. Understanding how these work can be broken down as follows:

All of the above decorators are a shortcut to the default_app routing decorators. For example, the @get() decorator applies bottle.default_app().get() to the handler.

routing decorators. For example, the decorator applies to the handler. The routing methods on default_app are all shortcuts for route() . So default_app().get('/') is equivalent to default_app().route(method='GET', '/') .

So @get('/') is the same as @route(method='GET', '/') , which is the same as @bottle.default_app().route(method='GET', '/') , and these can be used interchangeably.

One helpful thing about the @route decorator is that if you’d like, for example, to use the same handler to deal with both object updates and deletes, you could just pass a list of methods it handles like this:

@route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass

Alright then, let’s implement some of these handlers.

RESTful APIs are a staple of modern web development. Serve your API clients a potent concoction with a Bottle back-end.

POST: Resource Creation

Our POST handler might look like this:

import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})

Well, that’s quite a lot. Let’s review these steps part by part.

Body Parsing

This API requires the user to POST a JSON string at the body with an attribute named “name”.

The request object imported earlier from bottle always points to the current request and holds all of the request’s data. Its body attribute contains a byte stream of the request body, which can be accessed by any function that is able to read a stream object (like reading a file).

The request.json() method checks the headers of the request for the “application/json” content type and parses the body if it’s correct. If Bottle detects a malformed body (e.g.: empty or with wrong content type), this method returns None and thus we raise a ValueError . If malformed JSON content is detected by the JSON parser; it raises an exception that we catch and reraise, again as a ValueError .

Object Parsing and Validation

If there are no errors, we have converted the request’s body into a Python object referenced by the data variable. If we’ve received a dictionary with a “name” key, we’ll be able to access it via data['name'] . If we received a dictionary without this key, trying to access it will lead us to a KeyError exception. If we’ve received anything other than a dictionary, we’ll get a TypeError exception. If any of these errors occur, once again, we reraise it as a ValueError , indicating a bad input.

To check if the name key has the right format, we should test it against a regex mask, such as the namepattern mask we created here. If the key name isn’t a string, namepattern.match() will raise a TypeError , and if it doesn’t match it will return None .

With the mask in this example, a name must be an ASCII alphanumeric with no blanks from 1 to 64 characters. This is a simple validation and it doesn’t test for an object with garbage data, for example. More complex and complete validation may be achieved through the use of tools such as FormEncode.

Testing for Existence

The last test before fulfilling the request is whether the given name already exists in the set. In a more structured app, that test should probably be done by a dedicated module and signaled to our API through a specialized exception, but since we’re manipulating a set directly, we have to do it here.

We signal the existence of the name by raising a KeyError .

Error Responses

Just as the request object holds all the request data, the response object does the same for the response data. There are two ways of setting the response status:

response.status = 400

and:

response.status = '400 Bad Request'

For our example, we opted for the simpler form, but the second form may be used to specify the error’s text description. Internally, Bottle will split the second string and set the numeric code appropriately.

Success Response

If all the steps are successful, we fulfill the request by adding the name to the set _names , setting the Content-Type response header, and returning the response. Any string returned by the function will be treated as the response body of a 200 Success response, so we simply generate one with json.dumps .

GET: Resource Listing

Moving on from name creation, we’ll implement the name listing handler:

@get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})

Listing the names was much easier, wasn’t it? Compared to name creation there isn’t much to do here. Simply set some response headers and return a JSON representation of all names, and we’re done.

Now, lets see how to implement the update method. It’s not very different from the create method, but we use this example to introduce URI parameters.

@put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})

The body schema for the update action is the same as for the creation action, but now we also have a new oldname parameter in the URI, as defined by the route @put('/names/<oldname>') .

URI Parameters

As you can see, Bottle’s notation for URI parameters is very straightforward. You can build URIs with as many parameters as you’d like. Bottle automatically extracts them from the URI and passes them to the request handler:

@get('/<param1>/<param2>') def handler(param1, param2): pass

Using cascading route decorators, you may build URIs with optional parameters:

@get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass

Also, Bottle allows for the following routing filters in URIs:

int

Matches only parameters that may be converted to int , and passes the converted value to the handler: @get('/<param:int>') def handler(param): pass

float

The same as int , but with floating point values: @get('/<param:float>') def handler(param): pass

re (regular expressions)

Matches only parameters which match the given regular expression: @get('/<param:re:^[a-z]+$>') def handler(param): pass

path

Matches subsegments of the URI path in a flexible way: @get('/<param:path>/id>') def handler(param): pass Matches: /x/id , passing x as param .

, passing as . /x/y/id , passing x/y as param .

DELETE: Resource Deletion

Like the GET method, the DELETE method brings us little news. Just note that returning None without setting a status returns a response with an empty body and a 200 status code.

@delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return

Final Step: Activating the API

Supposing we’ve saved our names API as api/names.py , we can now enable these routes in the main application file main.py .

import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

Notice we’ve only imported the names module. Since we’ve decorated all the methods with their URIs attached to the default app, there is no need to do any further setup. Our methods are already in place, ready to be accessed.

You can use tools like Curl or Postman to consume the API and test it manually. (If you are using Curl, you can use a JSON formatter to make the response look less cluttered.)

Bonus: Cross Origin Resource Sharing (CORS)

One common reason to build a REST API is to communicate with a JavaScript front-end through AJAX. For some applications, these requests should be allowed to come from any domain, not just your API’s home domain. By default, most browsers disallow this behavior, so let me show you how to set up cross-origin resource sharing (CORS) in Bottle to allow this:

from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return

The hook decorator allows us to call a function before or after each request. In our case, to enable CORS we must set the Access-Control-Allow-Origin , -Allow-Methods and -Allow-Headers headers for each of our responses. These indicate to the requester that we will serve the indicated requests.

Also, the client may make an OPTIONS HTTP request to the server to see if it may really make requests with other methods. With this sample catch-all example, we respond to all OPTIONS requests with a 200 status code and empty body.

To enable this, just save it and import it from the main module.

Wrap Up

That’s all there is to it!

With this tutorial, I’ve tried to cover the basic steps to create a REST API for a Python app with the Bottle web framework.

You can deepen your knowledge about this small but powerful framework by visiting its tutorial and API reference docs.