Introduction
There are several ways to create an HTTP-based API today, whether that’s using an API gateway service from a cloud provider or using a framework such as Node.js. In this post, I’m going to demonstrate how to create an API in Python using API Star.
API Star is a Python framework that you can use to quickly create and deploy an HTTP API. Among other things, which are described on the project’s GitHub page, it is easy to get started, provides interactive API documentation generated from code so it’s always accurate, and provides a built-in type system that is both expressive and functional. I’ll demonstrate each of these below.
All the code shown below can be found on our GitHub page in the apistar_demo repository. You can find code for previous posts on our GitHub page as well.
Getting Started
First, setup a Python virtual environment and pip install apistar
. Next, initialize the project.
$ apistar new
This will create a WSGI-based application but you can also create an asyncio application. The latter is more challenging because any components within the application must be non-blocking.
Reviewing the Default Project
You should now have two files in your directory: app.py
and tests.py
. I like is that testing is built-in to
the framework, which encourages test-driven development (TDD). Let’s start the application as is and verify everything is as
expected.
$ apistar run
If you navigate to localhost:8080
, you should see the welcome message as shown below. It may render
slightly different, depending on your browser but the message should be the same.
{'message': 'Welcome to API Star!'}
Next, let’s take a look at the documentation at localhost:8080/docs
. This to me is one of the great features because it doesn’t require any additional
setup like you may have to do with something like Swagger. From here, you can also use the INTERACT
button to directly query the /
endpoint.
We’ll revisit the interaction later.
![API Star new project documentation]({{ “/img/apistar_welcome_docs.png” | absolute_url }})
Creating an S3 Bucket Endpoint
For this demo, we’ll be creating an endpoint that lists the S3 buckets available to a user. We’ll limit the number returned, which will be specified as a parameter to the endpoint. This should only be run locally as there isn’t any authentication or authorization enabled. This isn’t intended to be production-ready code.
Adding a New Endpoint
When I created this code, I wrote the tests first and then added the endpoint. For the purposes of this post, I’ll describe the endpoint first and then review the tests. If you haven’t used the moto AWS testing library before, see our [previous post on unit testing using moto]({% post_url 2018-03-25-unit-testing-python-code-that-uses-the-aws-sdk %}).
{% highlight python %} class Limit(typesystem.Integer): description = ‘The minimum (1) and maximum (10) number of buckets allowed to be returned.’ minimum = 1 maximum = 10
def list_buckets(limit: Limit) -> typing.List[str]: s3 = boto3.resource(‘s3’) return [bucket.name for bucket in s3.buckets.limit(limit)] {% endhighlight %}
First, let’s look at the class Limit
. Limit
inherits from the built-in type typesystem.Integer
from API Star. It defines a
data type that will be a parameter to the endpoint. It represents the minimum
and maximum
number of bucket names that may be returned. There is also
a description
attribute that provides an explanation of this parameter. We will also see this in the interactive documentation.
list_buckets
is the method that will return the names of the buckets. It accepts a Limit
argument and then queries
AWS using the boto3
library. It returns a list of str
, which contains the names of the buckets.
In order to add this method to the list of possible endpoints, we will create an entry in the routes
list. These are all
the possible endpoints our application will have. The routes
are used to create a WSGIApp
instance (App
is aliased to
WSGIApp
), called app
. Such app, I know. Lastly, the main
method, the entrypoint to our application, is called on app
.
{% highlight python %} routes = [ Route('/buckets’, ‘GET’, list_buckets), Include('/docs’, docs_urls), Include('/static’, static_urls) ]
app = App(routes=routes)
if name == ‘main': app.main() {% endhighlight %}
Testing
With API Star, you have two ways to test your endpoints: directly call the method or use the TestClient
. Directly testing
the method removes the client dependency and can allow you to more easily debug an issue. I’ve included
examples of each of the methods below.
Direct Testing
The test method below calls the list_buckets
method directly. Both direct and TestClient
testing use a helper method
that creates buckets that are used in the assert
statements in each test. It is included in both sections for clarity.
Here, we’re testing a range of valid values in order to ensure that the expected number of buckets is returned. For example, we expect a limit of three to return at most three buckets. In the test code, there are only two buckets so two are correctly returned with a limit of three.
{% highlight python %} @mock_s3 def test_list_buckets_directly_with_valid_limits(): create_buckets()
buckets = app.list_buckets(3)
assert len(buckets) == 2
buckets = app.list_buckets(2)
assert len(buckets) == 2
buckets = app.list_buckets(1)
assert len(buckets) == 1
@mock_s3 def create_buckets(): s3 = boto3.resource(‘s3’) s3.create_bucket(Bucket='resources.influentialcode.com’) s3.create_bucket(Bucket='testing.influentialcode.com’) {% endhighlight %}
Using the TestClient
The TestClient
allows for end-to-end testing because the request traverses the framework; this is helpful because it simulates
an actual client request and can expose issues that you may not have identified with direct testing.
The code below is a good example because we are explicitly allowing, using typesystem.Integer
, a Limit
parameter that
has a defined range of acceptable values. If you directly test the function, the Limit
value is meaningless in terms of
what is permitted.
The two tests below help to illustrate how to use TestClient
. The first test, test_list_buckets_with_client_valid_limit
, uses a value
within the defined range and verifies the response and status code that are received. In the second test, a value outside
the acceptable range is passed as an argument; 50
is greater than the maximum
that was defined in the class so
a response code of 400
is returned.
{% highlight python %} @mock_s3 def test_list_buckets_with_client_valid_limit(): create_buckets()
client = TestClient(app.app)
response = client.get('http://localhost/buckets?limit=1')
assert response.status_code == 200
assert response.json() == ['resources.influentialcode.com'] or response.json == ['testing.influentialcode.com']
@mock_s3 def test_list_buckets_with_client_invalid_limit(): create_buckets()
client = TestClient(app.app)
response = client.get('http://localhost/buckets?limit=50')
assert response.status_code == 400
@mock_s3 def create_buckets(): s3 = boto3.resource(‘s3’) s3.create_bucket(Bucket='resources.influentialcode.com’) s3.create_bucket(Bucket='testing.influentialcode.com’) {% endhighlight %}
Using the Interactive Documentation
![API Star interactive endpoint for /buckets]({{ “/img/apistar_s3_buckets_endpoint.png” | absolute_url }})
One of the benefits of API Star mentioned above is the built-in interactive documentation. I chose to create this demo code to illustrate
one aspect of that. As shown in the image below, in the interactive mode of the /buckets
endpoint, a number
input field is presented
to the user instead of a simple text field because an integer is expected for the limit
parameter. I didn’t do anything other
than inherit from the Integer
data type above in order for that to happen. This all built-in, which I think is a great feature
and can save developers time.
Now, let’s set a valid parameter and query the service. In order for this to work, you’ll need to have configured an AWS user
locally and allow it to list buckets (ListAllMyBuckets
) in IAM. This query returns the name of a single bucket
artifacts.influentialcode.com
because we specified a limit of 1.
![API Star response for valid query]({{ “/img/apistar_s3_buckets_valid_query.png” | absolute_url }})
Next, I’ll query using a value outside of that range (0). You receive a helpful message that the limit Must be greater than or equal to 1.
It’s incredibly helpful when an API tells you what went wrong instead of sending a 400
with something like
Message was invalid. Try again.
. When that happens, I usually think, “OK so what should I do differently? I have no idea what’s
wrong. I wouldn’t have tried if I thought it was wrong in the first place.".
![API Star response for invalid query]({{ “/img/apistar_s3_buckets_invalid_query.png” | absolute_url }})
Summary
API Star is a feature-rich Python framework that allows you to quickly develop and test an API. There are certainly other options that you can choose such as other frameworks or using an API gateway from a cloud provider. The benefit of the latter option is that you don’t have to choose a deployment target for the service; the cloud service does that for you. There are drawbacks though such as latency. What you choose will depend on your use case and resources available to you.
If you have questions or comments about this post, please leave them below. If you’d like to know when other posts are available, please follow us on Twitter.