Effortless REST with Flask, Part 2

American Tire Distributors
7 min readSep 27, 2020

In Part 1, we created the base scaffolding for a production-ready Flask API. But what’s an API without data, and if we’re serving sensitive data, how do we go about securing it? Effortless REST with Flask, Part 2 is all about data and security. Follow along step-by-step as we connect our API to a live database and secure our endpoints. As a reminder, You can access the code from the git repo here.

Let’s get started!

Connecting a Database

To make things simple, we’ll use Heroku to deploy a PostgreSQL database. Of course, your application can use any database connection string and is not tied to Heroku, but Heroku offers a free application offering along with a free PostgreSQL add-on. Follow these general steps to get set up on Heroku:

1. Sign up For Heroku / Log in to your Heroku account

2. Create a new app

3. Once created, Configure/Add the “Heroku Postgres” add on with the Hobby Dev Plan (free)

4. Add the Heroku CLI to your terminal

Once everything is configured, getting your Heroku database connection string is as easy as

heroku config:get DATABASE_URL -a {your_heroku_app_name}

The output of this command will be what we give our Flask SqlAlchemy plugin as our connection string. To make this easy, add the above command to the tasks.py script so you’ll always fetch the DATABASE_URL before starting your Flask application. You’ll also need to add your HEROKU_APP_NAME at the top of tasks.py.

Connecting to the Database

Now that we can start our server let’s take a look at how we can connect our database in chap-2/app/__init__.py.

We define a db variable, and then within the factory function, we initialize the database with the application. Because we've already set up our config to include an "SQLALCHEMY_DATABASE_URI" variable, we can use this simple command to connect our app to the database.

Defining Models

What’s a database without entities and models? Let’s view the User model in app/models/user.py.

"User Model"
# pylint: disable=no-member
from app import db
from sqlalchemy.sql import func

# A generic user model that might be used by an app powered by flask-praetorian
class User(db.Model):
__tablename__ = "users"

user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
roles = db.Column(db.Text)
is_active = db.Column(db.Boolean, default=True, server_default="true")
created_datetime = db.Column(
db.DateTime(), nullable=False, server_default=func.now()
)

@property
def rolenames(self):
try:
return self.roles.split(",")
except Exception:
return []

@classmethod
def lookup(cls, username: str):
return cls.query.filter_by(username=username).one_or_none()

@classmethod
def identify(cls, user_id: int):
return cls.query.get(user_id)

@property
def identity(self):
return self.user_id

def is_valid(self):
return self.is_active

def __repr__(self):
return f"<User {self.user_id} - {self.username}>"

What makes SqlAlchemy so powerful is that you can define the database model right here in Python code. You can define the table name, properties, and even other SQL native elements such as defaults and uniqueness. This allows you to interact with the database models and entities with ease in Python. If you don’t define a method here on the model, you can still import it and build queries elsewhere in your code.

Seeding the Database

Now that we’ve defined our connection string and defined our User model let’s seed our database. Thankfully, SQLAlchemy provides powerful commands to help stand up the database. These commands have been wrapped behind invoke commands. To initialize the database and seed the database, run the two following commands.

invoke init-db
invoke seed-db

These two commands are defined in the tasks.py file and create the tables in our database and seed them with two users.

Using SQLAlchemy Models to Query the Database

In the chap-2/app/__init__.py file, there is a /uhoh route that's defined where our user model is used.

Notice how we can import our User model at the beginning of our create_app function and then later use it within our route to construct a query that fetches all users. NOTE: If you're unfamiliar with the List[user] syntax, this is Python typing syntax with mypy.

Now that we’ve got a route defined and a model being queried let’s run a GET request by going to ‘http://127.0.0.1:5000/uhoh'.

You should see a response that says: TypeError: Object of type user is not JSON serializable

The TypeError tells us that our SQLAlchemy model response is not JSON serializable. In other words, the user variable can’t be transformed into JSON so easily. Instead, we need to deserialize the user and convert it into JSON. We can do this by using the Marshmallow package. Marshmallow is a foundational package when doing the input validation, deserialization into app-level objects, and serialization of app-level objects into Python types. We highly recommend that you get familiar with this package as it can offer you “superpowers” for app-objects coming in and out of your API.

Below is an example route that deserializes the SQLAlchemy model into JSON. It uses a defined schema to know how to deserialize the SQLAlchemy model.

When calling the route with http://127.0.0.1:5000/marsh, we see that the SQLAlchemy models correctly get transformed into JSON and the web browser displays the JSON.

Congratulations! You’ve now connected to a database and have an API serving data from it!

Authentication and Authorization

With our API connected to a database and serving data from it, let’s make sure that we can authenticate users before we use any sensitive data. To do this, we’ll use Flask Praetorian. Flask Praetorian is considered a “batteries included” JWT library. In other words, once you install this package, you can add authentication and authorization with little adjustments to your application. Let’s walk through the few steps we need to do to add JWT authentication.

In thechap-3/app/config.py file, add the JWT_ACCESS_LIFESPAN, and JWT_REFRESH_LIFESPAN config variable.

Then in the chap-3/app/__init__.py file, we can initialize the Praetorian library, add the /login route, and protect the routes with special decorator @auth_required.

"Main Flask App"
...
# Application Factory
# https://flask.palletsprojects.com/en/1.1.x/patterns/appfactories/
def create_app(config_name: str) -> Flask:
"""Create the Flask application
Args:
config_name (str): Config name mapping to Config Class
Returns:
[Flask]: Flask Application
"""
from app.config import config_by_name
from app.models import User
# Create the app
app = Flask(__name__)
# Log the current config name being used and setup app with the config
app.logger.debug(f"CONFIG NAME: {config_name}")
config = config_by_name[config_name]
app.config.from_object(config)
# Initialize the database
db.init_app(app)
# Initialize the flask-praetorian instance for the app
guard.init_app(app, User)

@app.route("/")
def hello_world() -> str: # pylint: disable=unused-variable
return "Hello World!"

@app.route("/login", methods=["POST"])
def login():
# Ignore the mimetype and always try to parse JSON.
req = request.get_json(force=True)
username = req.get("username", None)
password = req.get("password", None)
user = guard.authenticate(username, password)
ret = {"access_token": guard.encode_jwt_token(user)}
return (jsonify(ret), 200)

@app.route("/users", methods=["GET"])
@auth_required
def users():
users = User.query.all()
return jsonify(UserSchema(many=True).dump(users))

@app.route("/users/admin-only", methods=["GET"])
@roles_required("admin")
def users_admin():
users = User.query.all()
return jsonify(UserSchemaWithPassword(many=True).dump(users))

return app

We initialize the guard variable with our User SQLAlchemy model. This is the model that Flask Praetorian will use to authenticate users when doing guard.authenticate() in the /login route. When the user is authenticated, we can encode a JWT token from the user and send it back to the client. The client can then use that token when making requests to the backend, such as in /users route. The @auth_required decorated will check the request headers for an Authentication Bearer token. If the token is present, it’ll parse it and validate the user against the database. If the user is authenticated, then the route can proceed. When building production-ready applications, you should always consider what data will feed through the app. When we set up applications, we treat all data as if it’s sensitive, and users need to be authenticated. This is a great best practice to adopt.

Flask Praetorian also gives us authorization with the @roles_required decorator, as is demonstrated in the /users/admin-only route. This decorator authenticates the user and then verifies that the proper roles are present on the user before allowing the request to continue.

To see the above actions yourself, use the file called effortless_rest_with_flask_postman.json that allows you to import Postman requests to work with your local API quickly.

Securing your application can be incredibly challenging, but Flask Praetorian makes it simple. With a few endpoint configurations, you can easily add authentication and authorization. You now have an API that’s connected to a database along with routes that have general authentication and optional authorization. It would be perfectly reasonable to start building your business logic into your application now, but we still have something missing. How will our API consumers know about our API interfaces and inputs? What documentation can they rely on to be in concordance with our API? In Part 3 of this series, we’ll answer those questions with auto-generated API documentation within our Flask app. Until next time!

--

--

American Tire Distributors

Who we are as people is who we are as a company. We share the same values in the way we work with each other, our partners and our customers. We are ATD.