Building a Rest API with the Bottle Framework
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. In this article, I’ll provide a walkthrough of how to build a RESTful API service using Bottle.
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. In this article, I’ll provide a walkthrough of how to build a RESTful API service using Bottle.
Leandro has 15+ years in IT/development. Working with Python since 2013, he loves building efficient and cost-effective systems.
Previously At
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 appliesbottle.default_app().get()
to the handler. - The routing methods on
default_app
are all shortcuts forroute()
. Sodefault_app().get('/')
is equivalent todefault_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.
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.
PUT: Resource Update
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
, passingx
asparam
./x/y/id
, passingx/y
asparam
.
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.
Leandro Lima
São José dos Campos - State of São Paulo, Brazil
Member since November 19, 2015
About the author
Leandro has 15+ years in IT/development. Working with Python since 2013, he loves building efficient and cost-effective systems.
PREVIOUSLY AT