Flask API Quickstart Application with JSON Web Tokens, SQLAlchemy and Pytest
I created a sample Flask application that shows how to create HTTP API with authentication provided by JSON Web Tokens. It can be used to learn a bit about Flask, SQLAlchemy, JSON Web Tokens, Pytest and how it all works together. In particular, it shows how to:
- hash passwords using bcrypt
- issue and verify JWT with PyJWT
- create Flask decorator to require token authentication on endpoints
- use custom exceptions and an exception handler to return nice API responses on errors
- use Flask-SQLAlchemy and Flask-migrate to describe data models, query data, create data and migrations
- write simple tests in Pytest together with Flask test client
- document the API using the newest OpenAPI Specification 3
- use curl to query the API
The sample application is on Github in flask-api-quickstart repository. To start, clone it:
git clone git@github.com:stribny/flask-api-quickstart.git
The whole application is best described by the API specification that can be found in docs/api.yaml
. To get a nice visual sense of the API, put the contents of the file in Swagger editor.
Information about the requirements, installation, configuration and how to run the application are in the README file. In this blog post I want to cover implementation details and explain some choices I made.
App Structure
├── app # app package
│ ├── auth # auth package
│ │ ├── __init__.py
│ │ ├── helpers.py # helper functions
│ │ ├── exceptions.py # custom auth exceptions
│ │ ├── service.py # service layer for jwt, db, etc.
│ │ └── views.py # defines auth endpoints
│ ├── __init__.py # main init module
│ ├── exceptions.py # custom general exceptions
│ ├── config.py # main dev/test/production configuration
│ └── models.py # data model
├── docs
│ └── api.yaml # API documentation
├── LICENCE
├── migrations # contains generated migrations
│ ├── ...
├── Pipfile # app dependencies
├── Pipfile.lock
├── README.md
├── run.py # helper to invoke app package
└── tests # tests package
├── __init__.py
├── conftest.py # test configuration
├── helpers.py # helpers to reuse code
├── test_app.py # tests for app's main endpoints
└── test_auth.py # tests for auth endpoints
There are two main Python packages in the repository: app
which is our Flask application and tests
which stores our tests. When we run our application with python run.py
, our app’s init module app/__init__.py
is called. Here we can find our main initialization. The main Flask app object called app
is created here and configured with parameters from app/config.py
. We also create db
object that represents SQLAlchemy to issue commands to the database. We can now import these two objects elsewhere in the application and use them. The last defined object migration
is not going to be used programmatically, but it is necessary for migration commands to work (e.g. flask db upgrade
). In the rest of the init file we define application endpoints and error handlers. Note that here some of the imports are not placed at the top of the file to avoid circular imports.
Using Blueprint for auth endpoints
I separated authentication endpoints from the other ones using so called Blueprint. This Blueprint is defined in app/auth/views.py
as auth_api
and registered with the Flask application in the init file. This enables us to define the routes in a separate module and reuse a common URL prefix (/api/v1/auth). Blueprints are more powerful than what is used in the example, so be sure to check out the documentation to learn more about them.
Handling errors using custom exceptions and error handlers
In Flask it is typical to use abort()
function to indicate error. Abort function is configurable and so it could produce nice API error messages if we wanted to, but using custom exceptions instead is more flexible (e.g. exception can be suppressed on a higher layer of the application if desired). That’s why I decided to implement a few custom exception classes to represent errors. All of them derive from a base class called AppError
:
class AppError(Exception):
"""Base class for all errors. Can represent error as HTTP response for API calls"""
status_code = 500
error_code = "INTERNAL_ERROR"
message = "Request cannot be processed at the moment."
def __init__(self, status_code=None, error_code=None, message=None):
Exception.__init__(self)
if message is not None:
self.message = message
if status_code is not None:
self.status_code = status_code
if error_code is not None:
self.error_code = error_code
def to_api_response(self):
response = jsonify(
{"errorCode": self.error_code, "errorMessage": self.message}
)
response.status_code = self.status_code
return response
As we can see, this class stores status and error codes and an additional error message. It can also produce JSON response object. All custom error exceptions are defined in app/exceptions.py
and app/auth/exceptions.py
.
We also need to define exception handlers so that a correct output is sent when exception occurs. Here is an excerpt from the init file:
@app.errorhandler(404)
def custom404(error):
return NotFoundError().to_api_response()
@app.errorhandler(Exception)
def handle_exception(exception):
return AppError().to_api_response()
@app.errorhandler(AppError)
def handle_application_error(exception):
return exception.to_api_response()
The first exception handler is to handle general “Not found” error, when someone is trying to access a route that is not defined. The second one will catch all exceptions except our custom exceptions and will produce general “Internal error” response. The last one will be called whenever we programmatically raise one of our exceptions, e.g by calling raise InvalidCredentialsError()
.
Data model
There are two domain classes: User
that represents a registered user and BlacklistToken
for storing revoked tokens. Models are defined in app/models.py
using standard declarative approach available in SQLAlchemy.
Database calls are issued directly from the “service module” in app/auth/service.py
, because I prefer not to put the code for db calls in the models themselves (Active Record pattern). For more complicated cases I’d create a separate module that would sit between “service” and “model” layers of the application.
We have already seen the configuration needed to enable database migrations. The first database migration is already generated, so when the migration is run using flask db upgrade
, we end up with the database schema we need. When starting from scratch, you can generate migrations using these set of commands:
flask db init # when there are no migrations
flask db migrate # repeat on every schema change
flask db upgrade # install changes
JSON Web Tokens
Functions for working with JSON Web Tokens (encode_auth_token
, decode_auth_token
, etc.) can be found in app/auth/service.py
.
In app/auth/helpers.py
you will find an important part of the application: a decorator called auth_required
. This decorator will ensure that a valid token is provided when accessing routes that use it, e.g.:
@app.route("/protected")
@auth_required # requires valid JWT in the request
def protected():
return jsonify({"message": "Protected message"})
Testing
Writing tests using Pytest is pretty straightforward, but there is one thing that is needed to test Flask applications: Flask’s test client object that we use in the tests to set up data and call our endpoints:
def test_ping(client):
response = client.get("/ping")
...
This client
parameter is Pytest’s fixture, defined in tests/conftest.py
and automatically available in our test functions. This fixture is configured to set up our application with test configuration and set up the database tables before a test is executed, and clean up the database after it is run.
Final words
There are some advanced concepts in the example, but I tried to keep the application still very simple and straightforward. Some of the things in the application could be further divided into their own modules. For instance, the main endpoints could be placed in their own package instead of app/__init__.py
. Functions in helpers.py
and service.py
could be also separated by domain into their own separate files. And for sure, there are some missing pieces (e.g. logging). Despite the flaws I hope that the project will be useful for some of you!
For the curious minds, the source code is formatted using black, a new Python formatter, but I removed it from the development dependencies for now, because it is still in beta.
If you have any question regarding the source code or suggestion for improvement, feel free to open a Github issue with label “question” or leave a comment here. In case you are interested in other Flask materials, check out my other Flask-based sample app called Rockets.
Happy coding!
Last updated on 7.10.2018.