Creating a Rest API using the Bottle Framework

REST APIs are now a standard method for creating interfaces between the front-end and back-end of web applications, as well as between different web services. The widespread adoption of HTTP and HTTPS protocols across various networks and frameworks, combined with the simplicity of this interface style, makes it a natural choice when addressing interoperability concerns.

Bottle is a Python web framework known for its minimalist design. Its lightweight nature, speed, and ease of use make it well-suited for constructing RESTful services. In a bare-bones comparison conducted by Andriy Kornatskyy, it was ranked among the top three frameworks for response time and throughput (measured in requests per second). My own tests, conducted on DigitalOcean’s virtual servers, revealed that the combination of the uWSGI server stack and Bottle could achieve an overhead as low as 140μs per request.

This article provides a step-by-step guide on how to create a RESTful API service using Bottle.

Bottle: A Fast and Lightweight Python Web Framework

Setting Up and Configuring Bottle

The impressive performance of the Bottle framework is partly attributed to its lightweight design. The entire library is packaged as a single-file module. While this means it offers less hand-holding compared to other frameworks, it also provides greater flexibility, allowing it to integrate into diverse technology stacks. This makes Bottle an excellent choice for projects that prioritize performance and customizability, and where the time-saving advantages of more feature-rich frameworks are less critical.

Given Bottle’s flexibility, a detailed explanation of setting up the platform might not be relevant to your specific stack. However, let’s provide a brief overview of the available options and where to find more information on configuring them:

Installing Bottle

Installing Bottle is as simple as installing any other Python package. You have the following options:

  • Install it system-wide using your system’s package manager. For example, Debian Jessie (current stable release) offers version 0.12 as python-bottle.
  • Install it system-wide using the Python Package Index with the command pip install bottle.
  • Install it within a virtual environment (recommended).

To install Bottle in a virtual environment, you’ll need the virtualenv and pip tools. Installation instructions for these tools can be found in the virtualenv and pip documentation, although they are likely already present on your system.

In Bash, you can create an environment with Python 3 using the following command:

1
$ virtualenv -p `which python3` env

Note that omitting the -p `which python3` parameter will result in the installation defaulting to the system’s default Python interpreter, which is typically Python 2.7. Although Python 2.7 is supported, this tutorial assumes you are using Python 3.4.

Once the environment is set up, activate it and proceed with the Bottle installation:

1
2
$ . env/bin/activate
$ pip install bottle

That’s it! Bottle is now installed and ready for use. If you’re new to virtualenv or pip, their documentation is excellent. They are well worth exploring.

Choosing a Server

Bottle adheres to Python’s standard Web Server Gateway Interface (WSGI), making it compatible with any WSGI-compliant server. Some popular options include uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine, and more.

The exact setup process may differ slightly depending on the chosen environment. Bottle provides an object that conforms to the WSGI interface, and your server needs to be configured to interact with this object.

For more details on configuring your specific server, refer to the server’s documentation and Bottle’s documentation on here.

Database Considerations

Bottle is database-agnostic, meaning it doesn’t impose any restrictions on where your data resides. If you plan to use a database in your application, the Python Package Index offers a range of compelling options. These include libraries like SQLAlchemy, PyMongo, MongoEngine, and CouchDB for interacting with various databases, as well as Boto for working with DynamoDB. To get started, you’ll only need the appropriate adapter for your chosen database.

Getting Started with the Bottle Framework

Now, let’s dive into creating a basic application using Bottle. The code examples provided will assume you are using Python 3.4 or later. However, most of the concepts discussed should also apply to Python 2.7.

Here’s what a basic Bottle application looks like:

1
2
3
4
5
6
import bottle

app = application = bottle.default_app()

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

When we say “basic”, we truly mean it. This program doesn’t even greet you with a “Hello World”. Think about it, when was the last time a REST interface responded with “Hello World?” In this basic setup, any HTTP request sent to 127.0.0.1:8000 will receive a 404 Not Found response status.

Understanding Bottle Apps

While you can create multiple instances of Bottle apps, the framework provides a convenient default instance for you. Bottle maintains these instances internally in a stack. When you interact with Bottle, such as running the app or defining a route, without specifying a specific app instance, it defaults to this default app. In fact, the line app = application = bottle.default_app() is not strictly necessary in this example. However, it’s included here to demonstrate how to explicitly reference the default app when working with servers like Gunicorn, uWSGI, or other generic WSGI servers.

The concept of multiple apps might seem a bit puzzling initially, but it adds to Bottle’s flexibility. For different parts of your application, you can create specialized Bottle apps by instantiating other Bottle classes and configuring them as needed. These separate apps could be accessed through distinct URLs, thanks to Bottle’s URL routing mechanism. While we won’t delve into that in this tutorial, you’re encouraged to explore Bottle’s documentation on here and here for more in-depth information.

Running Your Bottle Application

The final line of the script is responsible for running your Bottle application using the specified server. If you don’t specify a server, as in this case, Bottle defaults to Python’s built-in WSGI reference server, which is suitable for development purposes but not recommended for production.

You can use a different server by modifying the last line as follows:

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

This provides a convenient way to start your app by simply running the script. For instance, if you saved this code as main.py, you can run python main.py to start the app using the specified server. Bottle provides several quite an extensive list of server adapters that can be used in this manner.

For WSGI servers that don’t have a specific Bottle adapter, you can start them using their own run commands. For instance, if you are using uWSGI, you would start it like this:

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

Organizing Your Project Structure

Bottle doesn’t impose a strict file structure for your application, giving you the freedom to organize it as you see fit. While my approach tends to evolve from one project to the next, I often find myself leaning towards an MVC-inspired structure.

Creating Your First REST API with Bottle

Now, let’s move beyond simply returning 404 errors and build a real REST API.

Imagine you want to create an interface to manage a collection of names. In a real-world scenario, you’d likely store these names in a database. For the sake of this example, we’ll keep things simple and use Python’s built-in set data structure.

Here’s a basic outline for our API. You can place this code anywhere within your project, but I recommend creating a dedicated API file, such as api/names.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

Defining API Routes

As you can see, Bottle leverages decorators for routing. The imported decorators (post, get, put, and delete) are used to register handler functions for their corresponding HTTP actions. Let’s break down how they work:

  • Each decorator is a shortcut to the corresponding routing decorator of the default_app instance. For instance, @get('/') is equivalent to bottle.default_app().get('/').
  • The routing methods on the default_app instance are themselves shortcuts for the more general route() method. This means default_app().get('/') is equivalent to default_app().route(method='GET', '/').

Therefore, you can use @get('/'), @route(method='GET', '/'), and @bottle.default_app().route(method='GET', '/') interchangeably.

The @route decorator provides additional flexibility. For example, if you want to handle both object updates and deletions with the same handler function, you can pass a list of methods:

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

Now, let’s move on to implementing some of these handler functions.

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

Handling POST Requests for Resource Creation

Here’s how you might implement the POST handler for creating new names:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import re, json

namepattern = re.compile(r'^[a-zA-Z\d]{1,64}

Let's dissect this code snippet step by step.

**_Parsing the Request Body_**

Our API expects the client to send a JSON string in the request body, containing an attribute named "name". 

The `request` object, which we imported earlier from `bottle`, provides access to the current request and all its associated data.  The `body` attribute of this object contains a byte stream of the request body.  You can interact with this attribute using any function designed to read from a stream object (similar to reading from a file).

The `request.json()` method simplifies things by checking if the request has a  "application/json" content type header and attempting to parse the body accordingly. If it encounters a malformed body (e.g., an empty body or incorrect content type), this method returns `None`. In such cases, we raise a `ValueError` to signal an error.  If the JSON parser itself encounters malformed JSON content, it raises an exception that we catch and re-raise as a `ValueError`.

**_Parsing and Validating the Data_**

Assuming no errors occurred during the previous step, we now have the request body parsed into a Python object, referenced by the `data` variable.  If we received a dictionary with a "name" key, we can access its value using  `data['name']`. If the received dictionary doesn't contain this key, attempting to access it will result in a `KeyError`. If we receive anything other than a dictionary, we'll encounter a `TypeError`.  In any of these error scenarios, we catch the exception and re-raise it as a `ValueError`, indicating invalid input. 

To ensure that the "name" key has the correct format, we use a regular expression mask. In this case, we define a simple mask called `namepattern`. The `namepattern.match()` method attempts to match the provided name against this mask.  If the "name" value isn't a string, this method will raise a `TypeError`. If it doesn't match the pattern, it will return `None`. 

The regular expression used in this example enforces that a name must consist of 1 to 64 alphanumeric ASCII characters without any spaces. This is a basic validation, and it might not cover all edge cases or protect against malicious input. You can achieve more robust and comprehensive validation using dedicated validation libraries like [FormEncode](http://formencode.readthedocs.org/en/latest/).

**_Checking for Existing Names_**

Before proceeding with fulfilling the request, we need to check if the provided name already exists in our set. In a more structured application, you would typically delegate this check to a separate module and handle it through a specific exception.  Since we're working directly with a set here, we perform this check directly within our handler.

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

**_Handling Error Responses_**

Similar to the `request` object for request data, Bottle provides a `response` object to manage response data. There are two ways to set the response status:

```python
response.status = 400

and:

1
response.status = '400 Bad Request'

In our example, we opted for the more concise form. However, the second form allows you to provide a custom text description for the error. Bottle internally splits the string and sets the numeric status code accordingly.

Generating a Success Response

If all checks pass, we add the name to our _names set, set the Content-Type response header, and return a response. Any string returned by the handler function is treated as the body of a 200 Success response. We use json.dumps to serialize our data into a JSON string.

Handling GET Requests for Resource Listing

Let’s move on to the name listing handler, which is much simpler:

1
2
3
4
5
6
7
@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 is straightforward. We set the appropriate response headers and return a JSON representation of all names in our set.

Handling PUT Requests for Resource Updates

Now, let’s explore how to implement the update method. While similar to the create method, it introduces the concept of URI parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@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 remains the same as the creation action. However, we now have an oldname parameter in the URI, as specified by the @put('/names/<oldname>') route.

Working with URI Parameters

Bottle makes working with URI parameters intuitive. You can define routes with as many parameters as needed, and Bottle will automatically extract their values from the URI and pass them to your handler function:

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

You can create routes with optional parameters using cascading route decorators:

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

Bottle also supports various routing filters to enforce constraints on URI parameters:

  • int

Ensures that the parameter can be converted to an integer and passes the converted value to your handler:

1
2
3
@get('/<param:int>')
def handler(param):
    pass
  • float

Similar to int, but for floating-point values:

1
2
3
@get('/<param:float>')
def handler(param):
    pass
  • re (regular expressions)

Matches only if the parameter conforms to the provided regular expression:

1
2
3
@get('/<param:re:^[a-z]+$>')
def handler(param):
    pass
  • path

Offers a flexible way to match subsegments of the URI path:

1
2
3
@get('/<param:path>/id>')
def handler(param):
    pass

For example, it can match both /x/id (passing “x” as param) and /x/y/id (passing “x/y” as param).

Handling DELETE Requests for Resource Deletion

The DELETE method is equally straightforward, much like the GET method. It’s worth noting that returning None without explicitly setting a status code will result in a response with an empty body and a 200 status code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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

Activating the API

Assuming you’ve saved your names API logic in a file named api/names.py, you can now activate these routes within your main application file, main.py.

1
2
3
4
5
6
7
import bottle
from api import names

app = application = bottle.default_app()

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

Note that we only import the names module here. This is because we’ve decorated all our handler functions with their respective routes and attached them to the default Bottle app. There’s no need for any further setup – our API methods are now accessible.

Nothing makes a front-end happy like a well-made REST API. Works like a charm!

You can test the API manually using tools like Curl or Postman. If you’re using Curl, consider piping the output through a JSON formatter to enhance readability.

Bonus: Enabling Cross-Origin Resource Sharing (CORS)

One of the primary motivations for building REST APIs is to facilitate communication with JavaScript front-ends via AJAX. In certain scenarios, you might need to allow these requests to originate from any domain, not just the domain hosting your API. Most browsers, by default, restrict this behavior for security reasons. Here’s how you can configure cross-origin resource sharing (CORS) in Bottle to enable this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 execute a function before or after each request. In this case, to enable CORS, we need to set the Access-Control-Allow-Origin, -Allow-Methods, and -Allow-Headers headers for every response. These headers inform the client about the allowed origins, methods, and headers for cross-origin requests.

Clients may also send an OPTIONS HTTP request to the server to inquire about permissible request methods. This snippet provides a catch-all example, responding to all OPTIONS requests with a 200 status code and an empty body.

To activate this CORS configuration, save it to a file and import it from your main module.

Conclusion

That concludes our whirlwind tour of building a REST API with Bottle.

We’ve covered the essential steps for creating a basic REST API for a Python application using the Bottle web framework.

For those eager to delve deeper into this compact yet powerful framework, be sure to explore the official Bottle tutorial and check out additional resources like API reference docs.

)

@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})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Let's dissect this code snippet step by step.

**_Parsing the Request Body_**

Our API expects the client to send a JSON string in the request body, containing an attribute named "name". 

The `request` object, which we imported earlier from `bottle`, provides access to the current request and all its associated data.  The `body` attribute of this object contains a byte stream of the request body.  You can interact with this attribute using any function designed to read from a stream object (similar to reading from a file).

The `request.json()` method simplifies things by checking if the request has a  "application/json" content type header and attempting to parse the body accordingly. If it encounters a malformed body (e.g., an empty body or incorrect content type), this method returns `None`. In such cases, we raise a `ValueError` to signal an error.  If the JSON parser itself encounters malformed JSON content, it raises an exception that we catch and re-raise as a `ValueError`.

**_Parsing and Validating the Data_**

Assuming no errors occurred during the previous step, we now have the request body parsed into a Python object, referenced by the `data` variable.  If we received a dictionary with a "name" key, we can access its value using  `data['name']`. If the received dictionary doesn't contain this key, attempting to access it will result in a `KeyError`. If we receive anything other than a dictionary, we'll encounter a `TypeError`.  In any of these error scenarios, we catch the exception and re-raise it as a `ValueError`, indicating invalid input. 

To ensure that the "name" key has the correct format, we use a regular expression mask. In this case, we define a simple mask called `namepattern`. The `namepattern.match()` method attempts to match the provided name against this mask.  If the "name" value isn't a string, this method will raise a `TypeError`. If it doesn't match the pattern, it will return `None`. 

The regular expression used in this example enforces that a name must consist of 1 to 64 alphanumeric ASCII characters without any spaces. This is a basic validation, and it might not cover all edge cases or protect against malicious input. You can achieve more robust and comprehensive validation using dedicated validation libraries like [FormEncode](http://formencode.readthedocs.org/en/latest/).

**_Checking for Existing Names_**

Before proceeding with fulfilling the request, we need to check if the provided name already exists in our set. In a more structured application, you would typically delegate this check to a separate module and handle it through a specific exception.  Since we're working directly with a set here, we perform this check directly within our handler.

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

**_Handling Error Responses_**

Similar to the `request` object for request data, Bottle provides a `response` object to manage response data. There are two ways to set the response status:

```python
response.status = 400

and:

1
response.status = '400 Bad Request'

In our example, we opted for the more concise form. However, the second form allows you to provide a custom text description for the error. Bottle internally splits the string and sets the numeric status code accordingly.

Generating a Success Response

If all checks pass, we add the name to our _names set, set the Content-Type response header, and return a response. Any string returned by the handler function is treated as the body of a 200 Success response. We use json.dumps to serialize our data into a JSON string.

Handling GET Requests for Resource Listing

Let’s move on to the name listing handler, which is much simpler:

1
2
3
4
5
6
7
@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 is straightforward. We set the appropriate response headers and return a JSON representation of all names in our set.

Handling PUT Requests for Resource Updates

Now, let’s explore how to implement the update method. While similar to the create method, it introduces the concept of URI parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@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 remains the same as the creation action. However, we now have an oldname parameter in the URI, as specified by the @put('/names/<oldname>') route.

Working with URI Parameters

Bottle makes working with URI parameters intuitive. You can define routes with as many parameters as needed, and Bottle will automatically extract their values from the URI and pass them to your handler function:

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

You can create routes with optional parameters using cascading route decorators:

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

Bottle also supports various routing filters to enforce constraints on URI parameters:

  • int

Ensures that the parameter can be converted to an integer and passes the converted value to your handler:

1
2
3
@get('/<param:int>')
def handler(param):
    pass
  • float

Similar to int, but for floating-point values:

1
2
3
@get('/<param:float>')
def handler(param):
    pass
  • re (regular expressions)

Matches only if the parameter conforms to the provided regular expression:

1
2
3
@get('/<param:re:^[a-z]+$>')
def handler(param):
    pass
  • path

Offers a flexible way to match subsegments of the URI path:

1
2
3
@get('/<param:path>/id>')
def handler(param):
    pass

For example, it can match both /x/id (passing “x” as param) and /x/y/id (passing “x/y” as param).

Handling DELETE Requests for Resource Deletion

The DELETE method is equally straightforward, much like the GET method. It’s worth noting that returning None without explicitly setting a status code will result in a response with an empty body and a 200 status code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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

Activating the API

Assuming you’ve saved your names API logic in a file named api/names.py, you can now activate these routes within your main application file, main.py.

1
2
3
4
5
6
7
import bottle
from api import names

app = application = bottle.default_app()

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

Note that we only import the names module here. This is because we’ve decorated all our handler functions with their respective routes and attached them to the default Bottle app. There’s no need for any further setup – our API methods are now accessible.

Nothing makes a front-end happy like a well-made REST API. Works like a charm!

You can test the API manually using tools like Curl or Postman. If you’re using Curl, consider piping the output through a JSON formatter to enhance readability.

Bonus: Enabling Cross-Origin Resource Sharing (CORS)

One of the primary motivations for building REST APIs is to facilitate communication with JavaScript front-ends via AJAX. In certain scenarios, you might need to allow these requests to originate from any domain, not just the domain hosting your API. Most browsers, by default, restrict this behavior for security reasons. Here’s how you can configure cross-origin resource sharing (CORS) in Bottle to enable this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 execute a function before or after each request. In this case, to enable CORS, we need to set the Access-Control-Allow-Origin, -Allow-Methods, and -Allow-Headers headers for every response. These headers inform the client about the allowed origins, methods, and headers for cross-origin requests.

Clients may also send an OPTIONS HTTP request to the server to inquire about permissible request methods. This snippet provides a catch-all example, responding to all OPTIONS requests with a 200 status code and an empty body.

To activate this CORS configuration, save it to a file and import it from your main module.

Conclusion

That concludes our whirlwind tour of building a REST API with Bottle.

We’ve covered the essential steps for creating a basic REST API for a Python application using the Bottle web framework.

For those eager to delve deeper into this compact yet powerful framework, be sure to explore the official Bottle tutorial and check out additional resources like API reference docs.

Licensed under CC BY-NC-SA 4.0