Eze Sunday Eze Sunday is a full-stack software developer and technical writer passionate about solving problems, one line of code at a time. Currently building Raveshift.com, a crypto exchange and tools for crypto payment solutions.

Creating Python REST APIs with Flask, Connexion, and SQLAlchemy

5 min read 1640

Python Logo Over a Dark Background

How useful is an API without documentation?

I could argue that it isn’t really useful, as it could be a pain for your coworkers to collaborate with you without proper documentation.

But most developers do not like writing documentation because it doesn’t really prevent their code from working. However, it’s one of the most important best practices to use when developing an API.

The good news is that if you decide to build your API with Flask, you can leverage the power of Connexion to develop your API and write the documentation at the same time because it forces you to write your documentation using OpenAPI specification, then maps your endpoint to your Python functions.

What is Connexion?

Connexion allows you to describe your REST API in detail, and you are practically guaranteed that it will work as you have specified.

To specify a Flask API route, you’ll have to use the @app.route decorator as shown below:

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

But with Connexion, you will write normal functions without the decorator and then map your function to the docs directly in a yaml file using the operationId:

paths:
  /hello_world:
    post:
      operationId: hello_world

So, let’s dig in.

Prerequisites

Setting up Flask

Setting up a Flask application is pretty straightforward. We need to install the required dependencies and set them up appropriately to work with Flask.

Let’s install them using pip.

We made a custom demo for .
No really. Click here to check it out.

pip install Flask marshmallow-sqlalchemy marshmallow flask-marshmallow "connexion[swagger-ui]"

In this tutorial, we’ll build a basic shipment API that allows us to create a shipment, view available shipments, and update shipments.

Building the database model in Flask

Let’s create the model for our API. We’ll use Flask-SQLAlchemy to create and manage our database model.

Our model.py will look like this:

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

class Shipment(db.Model):
    """
    Shipment Model
    """
    id = db.Column(db.Integer, primary_key=True)
    destination = db.Column(db.String(80))
    source = db.Column(db.String(120))
    current_location = db.Column(db.String(120))
    status = db.Column(db.String(120))
    item = db.Column(db.String(120))
    description = db.Column(db.String(120))
    tracking_number = db.Column(db.String(120), nullable=True)
    arrival = db.Column(db.String(120))
    def __repr__(self):
        return '<Shipment %r>' % self.item

    def __init__(self, description, source, current_location, status, item, tracking_number, arrival, destination):

        self.description =  description
        self.destination =  destination
        self.source = source
        self.current_location = current_location
        self.status = status
        self.item = item
        self.tracking_number = tracking_number
        self.arrival = arrival

Next, we’ll create the database schema with Marshmallow, a Python ORM that simplifies the process of converting complex data types such as objects to and from Python native data types.

Here’s the code for schema.py:

from flask_marshmallow import Marshmallow
ma = Marshmallow()
class ShipmentSchema(ma.Schema):
    """
    Schema
    """
    class Meta:
        fields = (
        'id', 
        'item', 
        'description', 
        'status', 
        'tracking_number',
        'current_location',
        'source',
        'destination',
        'description',
        'arrival'
        )

The database model fields should match what we have in the schema.

Next, set up a configuration file, where we’ll set up the basic application requirement. We’ll plug in a SQLite database and set up Connexion with Flask.

Here’s what config.py looks like:

import connexion
import os

basedir = os.path.abspath(os.path.dirname(__file__))
application = connexion.FlaskApp(__name__)
application.add_API("swagger.yaml")
app = application.app
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(basedir, 'db.sqlite')

Generally, when you set up a Flask app, you’ll initialize Flask, like so:

from flask import Flask
app = Flask(__name__)

However, because we are going to use Connexion, which will extend Flask’s features, we’ll do this differently. Let’s use Connexion to initialize Flask:

application = connexion.FlaskApp(__name__)

Next, we’ll add the API documentation yaml file, which will contain the entire route for our application.

application.add_API("swagger.yaml")

If you have your yaml file in a different directory, you can specify the exact directory by adding specification_dir='directory_name/' to theFlask Connexion instance:

application = connexion.FlaskApp(__name__, specification_dir='directory_name/')

To access the Flask app directly, we’ll reference it from the Connexion Flask instance:

app = application.app

Note that if we weren’t using Connexion, we’d just use application directly instead of application.app.

Finally, we’ll add the database configuration and set the path for our database file because we are using SQLite as our database.

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(basedir, 'db.sqlite')

We now have Connexion up and running with Flask!

Now let’s create the tables. Open your shell and run the following code to create all the tables specified in our model definition:

from app import db
db.create_all() 

Next, we’ll set up the main app entry point.

main.py

from app_config import app
from model import db
if __name__ == '__main__':
    db.init_app(app)
    app.run(debug=True)

Here, we’ll import the app, initialize the database, and run it. The app is now set up, so we just need to add the functions and routes.

Creating the routes in the .yaml file

Let’s start with creating the routes in the documentation. Begin by creating a swagger.yaml file in the app directory, the add the following OpenAPI specification:

swagger.yaml

openAPI: 3.0.0
servers:
  - description: Shipment Sample App
    url: http://localhost:5000/
info:
  description: This is a simple API
  version: "1.0.0"
  title: Simple shipment API
  contact:
    email: [email protected]your-company.com
  license:
    name: Apache 2.0
    url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
paths:
  /shipment/list:
    get:
      tags:
        - shipment
      summary: searches shipment
      operationId: view.get
      description: |
        By passing in the appropriate options, you can search for
        available shipment in the system
      responses:
        '200':
          description: search results matching criteria
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/shipmentItem'
        '400':
          description: bad input parameter

  /shipment/create:
    post:
      tags:
        - shipment
      summary: adds a shipment item
      operationId: view.post
      description: Adds an item to the system
      responses:
        '201':
          description: item created
        '400':
          description: 'invalid input, object invalid'
        '409':
          description: an existing item already exists
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/shipmentItem'
        description: shipment item to add

  /shipment/update/{id}:
    put:
      operationId: view.put
      tags:
        - shipment
      summary: Update a Shipment in the people list
      description: Update a Shipment in the Shipments list
      parameters:
        - name: id
          in: path
          description: Shipment ID
          required: true
          schema:
            type: integer
            format: int64

      responses:
        200:
          description: Successfully updated a shipment in Shipments list
components:
  schemas:
    shipmentItem:
      type: object
      properties:
        item:
          type: string
        description:
          type: string
        status:
          type: string
        tracking_number:
          type: string
        current_location: 
          type: string
        source:
          type: string
        destination":
          type: string
        arrival:
          type: string 

Let’s take a look at the paths that we’ll match with our functions. The functions will be matched with the operationId, as shown in the sample code below.

view is the file that contains all the API functions. The get function is the target for this particular route, so we use view.get.

/shipment/list:
    get:
      tags:
        - shipment
      summary: searches shipment
      operationId: view.get

We’ll have to put the get function we are referencing in the view.py file. This function will search the database to get all the shipments.

Here’s what the view.py code looks like:

def get(id=None):
    """
    get Shipment
    """
    try:
        if id is None:
            shipment = Shipment.query.filter().all()
            shipment_schema =  ShipmentSchema(many=True)
            return shipment_schema.jsonify(shipment)
        else:
            shipment = Shipment.query.filter_by(id=id).first()
            shipment_schema = ShipmentSchema()
            return shipment_schema.jsonify(shipment)
    except Exception as e:
        jsonify({"error":"There was an error please contact the administrator"})

We’ll do the same for other functions and endpoints. You can clone the repository on GitHub to see the full code for other functions. It’s basically the same pattern.

To update the shipment, we’ll need to pass an ID in the URL parameter. So how do we do that?

Let’s use the function for the put request as an example:

def put(id): #note that the name of this function can be anything, like update_shipment 
    """
    Update shipment
    """
    try:

        data = request.get_json()
        shipment = Shipment.query.filter_by(id=id).first()
        shipment = Shipment.query.filter_by(id=id)
        shipment.update(data)
        db.session.commit()

        return jsonify(data)
    except Exception as e:
        jsonify({"error":"There was an error please contact the administrator"})# Routes

The documentation will look like this. Notice that we are passing {id} in the route, as well as in the parameters object?

 /shipment/update/{id}:
    put:
      operationId: view.put
      tags:
        - shipment
      summary: Update a Shipment in the Shipments list
      description: Update a Shipment in the Shipments list
      parameters:
        - name: id
          in: path
          description: Shipment ID
          required: true
          schema:
            type: integer
            format: int64

      responses:
        200:
          description: Successfully updated a shipment in Shipments list

You can see that the route is expecting a parameter and that the parameter is defined in the parameters object as an ID.

You can now run your application with python main.py.

Once this is done, viola! Your API is ready, including the full documentation. By default, the API docs will be available in the route /ui.

Here is a screenshot of what we have created so far:

Simple Shipment API

You can even interact with the API directly from the API docs. I also deployed the API here for testing. Feel free to play around with it.

You can check out the entire source code on GitHub.

Conclusion

Building APIs and the documentation alongside one another has not always been easy, but Connexion has since made it possible. It’s also easy to integrate into Flask without having to learn an entirely new framework. Happy hacking!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Eze Sunday Eze Sunday is a full-stack software developer and technical writer passionate about solving problems, one line of code at a time. Currently building Raveshift.com, a crypto exchange and tools for crypto payment solutions.

Leave a Reply