API Star blog post intro image

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

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.

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)]

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.

routes = [
    Route('/buckets', 'GET', list_buckets),
    Include('/docs', docs_urls),
    Include('/static', static_urls)
]

app = App(routes=routes)

if __name__ == '__main__':
    app.main()

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.

@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')

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.

@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')

Using the Interactive Documentation

API Star interactive endpoint for /buckets

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

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

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.