This article provides a tutorial on building APIs with Falcon, a WSGI library for creating web APIs using the REST architectural style in Python.
Abstract
The article begins by introducing Falcon, a WSGI library for building web APIs using the REST architectural style in Python. It explains the terminologies of WSGI and REST and provides a step-by-step guide to creating APIs with Falcon. The tutorial covers installing Falcon and other required libraries, creating resources and responders, adding routes, and testing the APIs using Gunicorn, HTTPie, and cURL. The article also discusses advanced features such as suffixes, hooks, and middleware. The code for the article is available on GitHub, and related articles are provided for further reading.
Bullet points
Falcon is a WSGI library for building web APIs using the REST architectural style in Python.
WSGI stands for Web Server Gateway Interface, which is an interface specification by which web servers and applications communicate.
REST stands for Representational State Transfer, which is a software architectural style created to guide the design and development of the architecture for the World Wide Web.
The article provides a step-by-step guide to creating APIs with Falcon, including installing Falcon and other required libraries, creating resources and responders, adding routes, and testing the APIs using Gunicorn, HTTPie, and cURL.
The article also discusses advanced features such as suffixes, hooks, and middleware.
The code for the article is available on GitHub.
Related articles are provided for further reading.
Build APIs with Falcon in Python — All Essentials You Need
Learn a simple but efficient API framework in Python
Falcon is a WSGI library for building web APIs using the REST architectural style. Before we get started, let’s get familiar with two important terminologies, which can be mentioned in interviews and the Falcon documentation.
Photo by Erik van Dijk on Unsplash
WSGI — Web Server Gateway Interface. It is an interface specification by which web server and application communicate. A WSGI application is just a callable with a well-defined signature so that you can host the application with any web server that understands the WSGI protocol.
REST — REpresentational State Transfer. It is a software architectural style that was created to guide the design and development of the architecture for the World Wide Web. Any web service that obeys the REST constraints is informally described as RESTful.
This article covers the traditional synchronous flavor of Falcon using the WSGI protocol. Since this is a practice course for coding rather than a theoretical one, we won’t introduce any more abstract concepts. Now let’s get our hands dirty and start writing our first API with Falcon.
Before we get started, we need to install Falcon and other libraries needed for development. It’s better to install all the libraries specific to a project in a virtual environment so that they can be better maintained and won’t impact global system libraries. Besides, with a virtual environment, you can easily dockerize it and make it workable on different platforms. We will use the venv package of Python 3 to create the virtual environment.
Gunicorn will be used to host our App. If you are working on Windows, you can use Waitress as an alternative. However, I highly recommend using Linux or macOS to develop APIs because it is much more natural and convenient for testing and debugging in a Linux environment.
HTTPie is a user-friendly tool for making API requests. You can also use cURL but the results would be less readable. Since cURL is a more classical way of making API requests, it will also be introduced in this article. If you prefer a user interface, you can use Postman to make API calls.
Now we can start to write our first API. We need to create two files. The first one is the resource file (hello_world_api.py):
In spite of being such a small file, there is a massive amount of information in it.
Firstly, the resource is the central concept in the REST architectural style and the Falcon framework. It represents all the things (in other words, resources) that can be accessed by a URL in your API. In Falcon, a resource is just a regular Python class that includes one or more methods representing the standard HTTP verbs supported by that resource. We don’t need to inherit from any special base classes because Falcon uses duck-typing which means we need to define some methods to make the class special. You can name the resource class whatever you like, but it is better to give it a meaningful name reflecting its usage.
The special method here is the on_get() method. For any HTTP method you want your resource to support, simply add an on_*() method to the class, where * is any one of the standard HTTP methods, lowercased (e.g., on_get(), on_post(), on_put(), on_delete(), etc.). These on_*() methods are (informally) called responders in Falcon which will pass GET requests to on_get(), POST requests to on_post(), etc.
Each responder takes at least two parameters. The first parameter is always req which is an instance of falcon.Request. It can be used to read the headers, query parameters, and body of the request. The second one is always resp and is an instance of falcon.Response, which can be used for setting the status code, headers, and body of the response. Additional parameters can be obtained from the URL template as will be demonstrated later.
The default status code is 200 (falcon.HTTP_OK), more status codes can be found in this link. The default content type is JSON (falcon.MEDIA_JSON), and more media types can be found in this link.
The second one is the application file (app.py):
This file holds the creation of the application which is the entry point to our APIs. It’s by convention to call the application app . The other variable application is required by Gunicorn to work properly. If you don’t use Gunicorn to host your application, you don’t need to have the application variable defined.
The most important method of the application is add_route(), which associates a URL path with a resource. If a path requested by the client matches a URL template, the request will then be passed on to the associated resource for processing. Here if the user calls the /hello_world endpoint, HelloWorldResource will be used. Specially, we should use an instance of the resource class, not the class itself. The same instance is used for all requests. If the users make a GET request to /hello_world, the request will be routed to the on_get() method (called responder in Falcon). Similarly, POST requests to on_post(), PUT requests to on_put(), etc.
With the --reload option, Gunicorn restarts when any code is updated. This is handy for development.--timeout specifies the timeout to the server. After the time specified (in seconds), the server will disconnect the request. The timeout option is very important for debugging purposes with the rpdbmodule. If you don’t specify the timeout, the connection will be closed shortly and you won’t have enough time for debugging. Finally, we set the log level to DEBUG to show more verbose logging which is also helpful for development. If you use Gunicorn to host your application in production, you should set the timeout and log level to something best for performance.
Let’s use HTTPie to make a GET request to the /hello_world endpoint:
$ http GET localhost:8000/hello_world
Hmm, we don’t see the response body, and an Exception is raised on the server:
TypeError: {'result': 'Hello Word!'} is not a byte
The reason is that the data passed to resp.text should always be a string. It doesn’t have to be a binary string as the exception says because Falcon will handle it automatically. Actually, if it’s a binary string, you should use resp.data instead for better performance. Let’s use the json module to dump the dictionary to a JSON string:
This time it works as expected and the response body is displayed on the screen:
$ http GET localhost:8000/hello_world
HTTP/1.1200 OK
Connection: close
Date: Sun, 04 Jul 2021 22:41:39 GMT
Server: gunicorn
content-length: 25
content-type: application/json
{"result":"Hello Word!"}
You can also use resp.media to receive the dictionary directly without dumping it as a string, because we specify the content type as JSON. However, the response we see on the screen for HTTPie will be a raw string and not nicely formatted. Nonetheless, in practice, you may prefer to use resp.media because it can be more convenient sometimes.
We have created our first dummy API, which is not so interesting though. Let’s create some more practical APIs and add more features to them to make them more powerful.
Let’s create a set of APIs that can be used to create, read, update and delete laptop data. We won’t use any persistent data storage here for simplicity and better focus on the API issues. In practice, you can store the data in a database, either a traditional relational database like MySQL or a NoSQL database such as MongoDB, which are both free to use and are very popular in the industry.
Let’s create a new resource file called laptops.py, we will provide some dummy data to start with:
We created a new resource LaptopsResource, in which an on_get() responder is available to get all the laptop data.
Let’s add a route for this resource so that it can be accessed. In app.py add the following lines:
Use Gunicorn to start the app if it’s not done yet. Then call the /laptops endpoint with HTTPie:
$ http GET localhost:8000/laptops
We should see a nicely formatted list of laptop data returned on the screen.
[
{
"id": 1,
"name": "HP EliteBook Model 1",
"price": 8842.0
},
{
"id": 2,
"name": "Lenovo ThinkPad Model 2",
"price": 18886.0
},
{
"id": 3,
"name": "Apple MacBook Air Model 3",
"price": 16795.0
}
]
Now let’s create a POST responder which can be used to create a new laptop and add it to the list. In laptops.py, add:
Specially, req.bounded_stream is a file-like input object for reading the body of the request. Technically req.bound_stream is just a file-like wrapper around req.stream which is the real file-like input object for reading the body of the request. req.bound_stream is better than req.stream because it is aware of the expected content length of the body, and will never block on out-of-bounds reads, which makes it safer to use. Besides, we use json.load to load a JSON object (a dictionary in Python) from a file or file-like object.
The content type of the response is falcon.MEDIA_TEXT because we just want to return a string in the response.
We don’t need to update app.py because the GET and POST requests use the same URL path and resource. The only difference is the HTTP methods that are used and the corresponding responders that will be called.
Let’s add a new product:
$ http POST localhost:8000/laptops id=4 name="Dell Inspiron Model 4"price=20000.0
With HTTPie, we don’t need to specify the content type if it’s JSON because it’s the default content type with HTTPie. The JSON object for the POST request is provided as key/value pairs in the format key=value. Multiple key/value pairs are separated by empty spaces. You can check the HTTPie documentation for more usage examples. If you prefer the traditional curl command, you can use this one:
As we can see, the curl command can be a little more tedious to write than http. If you often need to write POST requests, you may want to use a graphical interface such as Postman which is much easier to use and maintain.
In LaptopsResource, we can only access all the laptop information, if we want to access the information of an individual laptop, we would need to define a new resource.
This time let’s first define the route. In app.py, add:
The special _id parameter in curly brackets is a dynamic route parameter and will be passed to the responder as it is. All responders (namely on_*() methods, *=get, post, put, delelte, etc) will get a parameter called _id. Here we use _id because id is a built-in keyword in Python and should not be used.
Let’s create the resource and the responder in laptops.py:
We use the built-in filter function in Python 3 to filter a list. The route parameter is always passed in as a string and thus we need to convert it to an integer for it to work properly in the filter. In this example, we can have multiple items with the same id because we don’t implement any limitations for it. In practice, if you store the data in the database you will make the id a primary key and there can only be a single item with the same id.
Let’s create another responder in LaptopResource to update a laptop with a specified id. Now, something important if you are new to API development. We should always use the POST method to create a new item, and the PUT method to update an existing one:
Like the POST request, we also need to provide a JSON body to the PUT request. However, this time only the fields to be updated need to be provided, not the whole laptop object, let’s update the price of a laptop:
Now we have built a fully functional set of APIs which can be used to do CRUD operations for our laptop objects. We can use the Inspect Module to visualize the configuration of our application :
$ falcon-inspect-appapp:app
Falcon App (WSGI)
• Routes:
⇒ /hello_world - HelloWorldResource:
└── GET - on_get
⇒ /laptops - LaptopsResource:
├── GET - on_get
└── POST - on_post
⇒ /laptops/{_id} - LaptopResource:
├── DELETE - on_delete
├── GET - on_get
└── PUT - on_put
When the application becomes bigger, you may want to use some advanced features to reduce code redundancy or realize more complex features. We will only cover the suffix, hook, and middleware features here, which are most commonly used.
We can also write the responders for individual laptops in LaptopsResource with the suffix feature. To do this, we first need to add a suffix parameter to the add_route method:
Note that we need to comment out the original route for /laptops/{_id} using LaptopResource (note it’s singular here).
For the corresponding responders, the suffix is the suffix of the responder. This is where the name of the feature comes from:
Since we already have an on_get() responder for the GET method to get all the laptops' information, if we don’t use a suffix, Falcon won’t know which responder should be used for the provided route. If a suffix is provided, Falcon will map GET requests to on_get_{suffix}(), POST requests to on_post_{suffix}(), etc. In this way, multiple closely related routes can be mapped to the same resource.
With this update, the APIs should work exactly the same as before.
We can implement some custom logic before or after a request using a hook. Let’s create a hook to check the provided data for the POST request:
The @falcon.before decorator means that before every call to the responder ( on_post()), Falcon will first invoke validate_laptop_input() to check the request. If you want to have a quick start or refresher on how decorators work in Python, you can check this article.
We use req.context here because we want to pass the parsed data rather than the raw string to the responder. In order to pass data from a hook or middleware function to a resource function, we should use the req.context objects, which are intended to hold request and response data specific to your app as it passes through the framework.
Every hook takes four arguments. The first two is the same req and resp objects that are passed into responders. The resource argument is a resource instance associated with the request. The fourth argument params is a reference to the kwarg dictionary Falcon creates for each request, which will contain the route’s URI template params and their values if any. The four arguments should all be provided even if they are not used.
Let’s try to make POST requests with invalid and valid inputs and see what will happen:
As a homework task, you can create a hook for the PUT responder to check and validate the data passed to it.
The last feature I want to introduce is middleware, which may sound like a mysterious name but it is actually not that complex. Middleware works similarly to hooks, which also provides a way to execute some logic before the framework routes each request after it is routed but before the target responder is called, or just before the response is returned for each request. However, unlike hooks which are applied to specific responders, middleware methods are applied globally to the entire App, namely to all responders in all resources.
A middleware component is just a regular Python class that implements one or more of the predefined event handler methods, namely process_request, process_resource, and process_response. The Falcon documentation provides a nice explanation for these methods.
Let’s create a custom middleware component to automatically serialize all the responses. In this example, you can use resp.media to automatically serialize your data if you specify the content type as falcon.MEDIA_JSON. However, if you have some special data type in your response, such as custom objects, then the default serializer would fail.
Let’s add a new shop field to the laptops list. The value for the shop field is an instance of a dummy Shop class:
If we call the GET request to get all the laptops, we will get a server error: 500 Internal Server Error. The error on the backend is TypeError: Object of type 'Shop' is not JSON serializable. In this case, we need to provide a custom serializer to parse the response data correctly.
We should change resp.text to resp.media and return the original data which will be processed by the event handlers in the middleware:
We should normally create a separate file for the middleware component:
Similar to the hooks, we need to provide some pre-defined parameters to the event handlers. Especially, different event handlers have different pre-defined parameters. For process_response, it has four parameters, namely req, resp, resource and req_succeeded. All four parameters need to be present even if they are not used. req, resp and resource has the same meanings as in the hooks explained above. req_succeeded is specific for process_response and has the value of True if no exceptions were raised while the framework processed and routed the request.
Then we should add an instance of the middleware component to middleware property of the App class:
Same as the resource in add_route(), an instance of the middleware component class is expected, rather than the component class itself.
Now if you try to run:
$ http GET localhost:8000/laptops
The request will finish successfully and the result will be formatted nicely on your screen:
You can add more middleware to check, validate or update your requests or responses. Note that if multiple middlewares are added, the event handlers of them are executed in a predefined order.
In this article, the essentials of writing APIs with the Falcon framework in Python are introduced and a step-by-step tutorial is provided for writing APIs with Falcon. The official documentation of the Falcon API is good but can be too complex for beginners. I myself was overwhelmed by the API tests in the tutorial in the beginning and spent more time than needed to master the essentials of the Falcon framework to get started reading and writing APIs in Falcon. I will have a dedicated article for writing unit tests in Python so you can master both API design and unit tests with no pressure. I hope this article can save you some time and you can start to write your APIs immediately after reading this article. Many links have been provided in the article for the key concepts and more complex features that can be referenced when needed.
The code for this article can be found on GitHub. Note that some parts have been updated to make them work properly with the middleware.