Flask, Connexion and SQLAlchemy (part 1)

Python REST API: Flask, Connexion and SQLAlchemy (Part 1)

This translation of the article from Philip Acsany

Most modern web applications work on the basis of REST API – a methodology that allows developers to separate the development of the user interface (FrontEnd) from the development of internal server logic (BackEnd), and users receive an interface with dynamically loaded data. In this three-part series, you'll create a REST API using the Flask web framework.

In this first part of the series, you will learn how to:

  • Create a basic REST API project in Flask

  • Handle HTTP requests using Connexion

  • Define API endpoints using the OpenAPI specification

  • Interact with your API to manage data

  • Create API annotations using Swagger UI

After completing the first part, in the second you will learn how to use a database to store data permanently instead of relying on RAM.

Terms of reference

Create an application to manage postcards for characters from whom you can receive gifts throughout the year. These fabulous faces are: Tooth Fairy, Easter Bunny and Knecht Ruprecht.

Ideally, you want to be on good terms with all three of them, which is why you will send them cards to increase your chances of receiving valuable gifts from them.

Part 1 plan

In addition to implementing the postcard list, you're going to create a REST API that provides access to the list of characters and the individual characters within that list. Here is the API design for the characters:

Action

HTTP method

URL

Description

Read

GET

/api/people

Reading the Character List

Create

POST

/api/people

Creating a new character

Read

GET

/api/people/<lname>

Retrieving character data

Update

PUT

/api/people/<lname>

Updating an existing character

Delete

DELETE

/api/people/<lname>

Deleting an existing character

The character data structure that will be used in the REST API application being developed is as follows (characters are identified by last name, and any changes are marked with a timestamp):

PEOPLE = {
    "Fairy": {
        "fname": "Tooth",
        "lname": "Fairy",
        "timestamp": "2022-10-08 09:15:10",
    },
    "Ruprecht": {
        "fname": "Knecht",
        "lname": "Ruprecht",
        "timestamp": "2022-10-08 09:15:13",
    },
    "Bunny": {
        "fname": "Easter",
        "lname": "Bunny",
        "timestamp": "2022-10-08 09:15:27",
    }
}

Let's go!

In this section, you will prepare a development environment for a Flask REST API project. First, you will create a virtual environment and install all the dependencies needed for the project.

Creating a virtual environment

In this section you will create the structure of your project. You can name your project's root folder any way you like. For example, you can call it rp_flask_api. Create a folder and go to it:

mkdir rp_flask_api
cd rp_flask_api

The files and folders you create during this series will be located either in this folder or in its subfolders.

Once you're in the project folder, it's a good idea to create and activate a virtual environment. This way, you install all project dependencies not on the entire system, but only in the virtual environment of your project.

python -m venv venv
.\venv\Scripts\activate
python -m venv venv
source venv/bin/activate

As a result, a folder will be created in your project folder venv with the virtual environment files, as well as in the operating system prompt, the value should appear (venv) which means that the virtual environment from your project folder is activated and you are working in it.

Adding dependencies

After installing and activating the virtual environment, you need to add using pip Flask library for development:

pip install Flask==2.2.2

The Flask micro web framework is the main dependency your project requires. On top of Flask, install Connexion to handle HTTP requests:

pip install "connexion[swagger-ui]==2.14.1"

Also, to use the auto-generated API documentation, you install Connexion with Swagger UI support added.

Creating a starter Flask project

The main file of your Flask project will be app.py. Create app.py V rp_flask_api and add the following content:

# app.py

# импорт модуля Flask, который вы ранее установили с помощью
# pip install Flask==2.2.2 "connexion[swagger-ui]==2.14.1"
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/") # декоратор функции для "/" (корневого URL веб-приложения)
def home():
    return render_template("home.html") # функция, выводящая home.html в качестве шаблона

if __name__ == "__main__":
    # основной вызов приложения с указанием хоста и порта
    app.run(host="0.0.0.0", port=8000, debug=True)

For a Flask application you need to create a file home.html in a template directory named templates. Create a directory templates in the root directory of the application and add the following there home.html:

<!-- templates/home.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>RP Flask REST API</title>
</head>
<body>
    <h1>
        Hello, World!
    </h1>
</body>
</html>

Flask comes with the Jinja Templating Engine, which allows you to enhance your templates. But your template home.html is a simple HTML file without any Jinja functionality. For now it's ok because the goal is home.html — check that your Flask project delivers everything to the browser as intended.

With the Python virtual environment activated, you can run your application using this command line in the directory containing the file app.py:

python app.py

On startup app.py the web server will start on port 8000. If you open your browser and go to http://localhost:8000you should see the Hello, World! message:

Great, your web server is up and running! Later you will expand the file home.html to work with the REST API you are developing.

By now, your Flask project structure should look like this:

rp_flask_api/
│
├── templates/
│   └── home.html
│
└── app.py

This is a great framework to start any Flask project. You may find the source code useful when working on future projects. In the following sections, you'll expand the project and add your first REST API endpoints.

Adding the first REST API endpoint

Now that you have a working web server, you can add your first REST API endpoint. To do this, you will use Connexion, which you installed in the previous section.

The Connexion module allows a Python program to use the OpenAPI specification with Swagger. The OpenAPI specification is an API description format for REST APIs that provides many features including:

  • Validating input and output data to and from your API

  • Configuring URL API endpoints and expected parameters

When using OpenAPI with Swagger, you create a user interface (UI) to describe the API's functionality by creating a configuration file that your Flask application can access.

Creating an API Configuration File

A Swagger configuration file is a YAML or JSON file containing OpenAPI annotations. This file contains all the information needed to configure the server to provide validation of input parameters, validation of response output, and determination of the URL endpoint.

Create a file named swagger.yml and start adding metadata to it:

# swagger.yml

# задание версии OpenAPI: важно, так как могут меняться от версии к версии
openapi: 3.0.0
info: # информационный блок
  title: "RP Flask REST API" # заголовок
  description: "An API about people and notes" # описание
  version: "1.0.0" # версия вашего API

Next in the block servers add urls that define the path to your API:

# swagger.yml

# ...

servers:
  - url: "/api"

By indicating "/api" as a value urlyou will be able to access all API paths at http://localhost:8000/api.

Next, you define the API endpoints in the paths block path:

# swagger.yml

# ...

paths:
  /people:
    get:
      operationId: "people.read_all"
      tags:
        - "People"
      summary: "Read the list of people"
      responses:
        "200":
          description: "Successfully read people list"

Block paths defines the configuration of URL API endpoint paths:

/people: Relative URL of the API endpoint
get: The HTTP method this endpoint will respond to with the URL
Together with the URL definition, this creates an endpoint with a URL GET /api/peoplewhich you can access at http://localhost:8000/api/people.

Block get defines the configuration of a single URL endpoint /api/people:

operationId: Python function that will respond to the request
tags: Tags assigned to this endpoint that allow operations to be grouped in the user interface
summary: Annotation text in the UI for this endpoint
responses: Status codes that the endpoint responds with
operationId must contain a string. Connexion will use "people.read_all" to search for a Python function named read_all() in the module people your project. You'll create the corresponding Python code later in this tutorial.

The response block defines the configuration of possible status codes. This is where you define a successful response for the status code «200»containing some description text.

You can find the full contents of the swagger.yml file in the swagger file below:

Full code of the swagger.yml file
# swagger.yml

openapi: 3.0.0
info:
  title: "RP Flask REST API"
  description: "An API about people and notes"
  version: "1.0.0"

servers:
  - url: "/api"

paths:
  /people:
    get:
      operationId: "people.read_all"
      tags:
        - "People"
      summary: "Read the list of people"
      responses:
        "200":
          description: "Successfully read people list"

This file is organized hierarchically. Each left indent represents a certain level of nesting: like in the Python hierarchy.

For example, paths marks the beginning where all API URL endpoints are defined. Meaning /people with an indent below it represents the beginning where all URL endpoints will be defined /api/people. Scope get: indented below /people contains definitions associated with an HTTP GET request to a URL endpoint /api/people. Organizing a hierarchy in a similar way is typical for all YAML files.

File swagger.yml is like a blueprint for your API. With the specifications you include in swagger.yml, you define what data your web server can expect and how your server should respond to requests. But for now, your Flask project doesn't know about your swagger.yml file. Read on to use Connexion to connect the OpenAPI specification to your Flask application.

Adding Connexion to your application

Adding a REST API URL endpoint to a Flask application using Connexion involves two steps:

To connect the API configuration file to your Flask application, you need to reference swagger.yml in the file app.py:

# app.py

from flask import render_template # удаляем: import Flask
import connexion # добавляем: connexion

# создание экземпляра приложения с использованием Connexion, а не Flask
# Внутри приложение Flask все еще создается, но теперь к нему добавлены
# дополнительные функции.
app = connexion.App(__name__, specification_dir="./")
app.add_api("swagger.yml")

@app.route("/")
def home():
    return render_template("home.html")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=True)

Receiving Data from the Character Endpoint

In file swagger.yml you have configured Connexion with operationId value "people.read_all". So when the API receives an HTTP request GET /api/peopleyour Flask application calls a function read_all() in the module people.

To make this work, create a file people.py with function read_all():

# people.py

from datetime import datetime

def get_timestamp(): # функция, возвращающая текущее время в заданном формате
    return datetime.now().strftime(("%Y-%m-%d %H:%M:%S"))

PEOPLE = { # словарь со значениями Персонажей
    "Fairy": {
        "fname": "Tooth",
        "lname": "Fairy",
        "timestamp": get_timestamp(),
    },
    "Ruprecht": {
        "fname": "Knecht",
        "lname": "Ruprecht",
        "timestamp": get_timestamp(),
    },
    "Bunny": {
        "fname": "Easter",
        "lname": "Bunny",
        "timestamp": get_timestamp(),
    }
}

def read_all(): # сервер запускает эту функцию при вызове /api/people
    return list(PEOPLE.values())

After launching the application, in the browser at http://localhost:8000/api/people you will get the following output:

Great, you've created your first API endpoint! Before you continue down the path to creating a REST API with multiple endpoints, take a moment to explore the API a little more in the next section.

A little about the API documentation

Currently you have a REST API running on a single URL endpoint. Your Flask application knows what to serve based on your API specification in swagger.yml. Additionally, Connexion uses swagger.yml to create API documentation for you.

Go to localhost:8000/api/uito see the API documentation in action:

This is the initial Swagger interface. It shows a list of URL endpoints supported in your endpoint http://localhost:8000/api. Connexion creates it automatically when parsing the file swagger.yml.

If you click on the end point /people in the interface, the interface will expand to show more information about your API: this will display the structure of the expected response, the content type of that response, and the descriptive text you entered for the endpoint in the file swagger.yml. Every time the configuration file changes, the Swagger UI also changes.

You can even try using the endpoint by clicking a button Try it out. This feature can be extremely useful as your API grows. The Swagger UI API documentation gives you the ability to explore and experiment with the API without having to write any code to do so.

Using OpenAPI with Swagger UI offers a nice, clean way to create URL API endpoints. So far, you've only created one endpoint to serve all the characters. In the next section, you'll add additional endpoints for creating, updating, and deleting specific characters in your list.

Creating a Full API

Until now, your Flask REST API had one endpoint. Now it's time to create an API that provides full CRUD access to your people structure. As you remember, your API plan looks like this:

Action

HTTP method

URL

Description

Read

GET

/api/people

Reading the Character List

Create

POST

/api/people

Creating a new character

Read

GET

/api/people/<lname>

Retrieving character data

Update

PUT

/api/people/<lname>

Updating an existing character

Delete

DELETE

/api/people/<lname>

Deleting an existing character

To do this you will need to expand the files swagger.yml And people.py to fully support the API defined above.

Working with Components

Before you define new API paths in swagger.ymladd a new block components for components. Components are the building blocks in your OpenAPI specification that you can reference from other parts of your specification.

Add a component block with schematics for one character:

# swagger.yml

openapi: 3.0.0
info:
  title: "RP Flask REST API"
  description: "An API about people and notes"
  version: "1.0.0"

servers:
  - url: "/api"

components:
  schemas:
    Person:
      type: "object"
      required:
        - lname
      properties:
        fname:
          type: "string"
        lname:
          type: "string"
# ...

To avoid code duplication, you create a component block. At this point you are only saving the data model Person in the diagram block:

Dash (-) before – lname indicates that required may contain a list of properties. Any property that you define as requiredmust also exist in properties including the following:

Key type defines the value associated with its parent key. For Person all properties are strings. You'll introduce this schema in your Python code as a dictionary later in this tutorial.

Creating a new character

Extend the API endpoints by adding a new block for the POST request to the block /people:

# swagger.yml

# ...

paths:
  /people:
    get:
        # ...
    post:
      operationId: "people.create"
      tags:
        - People
      summary: "Create a person"
      requestBody:
          description: "Person to create"
          required: True
          content:
            application/json:
              schema:
                x-body-name: "person"
                $ref: "#/components/schemas/Person"
      responses:
        "201":
          description: "Successfully created person"

Structure for post similar to existing circuit get. One difference is that you also send requestBody to the server. Ultimately, you need to tell Flask the information it needs to create a new character. Another difference is operationIdwhich you set to people.create.

Inside the content you define application/json as your API data exchange format.

You can serve different types of media in your API requests and API responses. Currently, APIs typically use JSON as their data interchange format. This is good news for you as a Python developer because JSON objects are very similar to Python dictionaries. For example:

{
    "fname": "Tooth",
    "lname": "Fairy"
}

This JSON object resembles the Person component you defined earlier in swagger.yml and which you refer to with $ref in the diagram.

You also use the HTTP status code 201, which is a success response indicating the creation of a new resource.

If you want to learn more about HTTP status codes, you can check out Mozilla's documentation on HTTP response codes.

By using people.create you tell the server to look for a function create() in the module people. Open the file people.py and add the function create() :

# people.py

from datetime import datetime
from flask import abort # импорт функции abort из Flask

# ...

def create(person):
    lname = person.get("lname")
    fname = person.get("fname", "")

    if lname and lname not in PEOPLE:
        PEOPLE[lname] = {
            "lname": lname,
            "fname": fname,
            "timestamp": get_timestamp(),
        }
        return PEOPLE[lname], 201
    else:
        # использование abort() помогает отправить сообщение об ошибке
        # когда тело запроса не содержит фамилию или когда человек с такой
        # фамилией уже существует.
        abort(
            406,
            f"Person with last name {lname} already exists",
        )

The person's last name must be unique because you are using lname as a PEOPLE dictionary key. This means that at this point in your project there cannot be two people with the same last name.

If the data in the request body is valid, you update PEOPLE on line 13 and respond with a new object and HTTP code 201 on line 18.

Character Processing

Open swagger.yml and add the following code:

# swagger.yml

# ...

components:
  schemas:
    # ...
  parameters:
    lname:
      name: "lname"
      description: "Last name of the person to get"
      in: path
      required: True
      schema:
        type: "string"

paths:
  /people:
    # ...
  /people/{lname}:
    get:
      operationId: "people.read_one"
      tags:
        - People
      summary: "Read one person"
      parameters:
        - $ref: "#/components/parameters/lname"
      responses:
        "200":
          description: "Successfully read person"

Like the path /peopleyou start with an operation get for the way /people/{lname}. Substring {lname} is a placeholder for last name, which you must pass as a URL parameter. So for example the URL path api/people/Ruprecht contains Ruprecht How lname.

URL parameters are case sensitive. This means you must enter the surname, for example Ruprecht with a capital R.

Parameter lname you will use in other operations as well. So it makes sense to create a component for it and reference it when needed.

operationId indicates a function read_one() V people.pyso go to that file again and create the missing function:

# people.py

# ...

def read_one(lname):
    if lname in PEOPLE:
        return PEOPLE[lname]
    else:
        abort(
            404, f"Person with last name {lname} not found"
        )

When your Flask application finds the specified last name in PEOPLE, it returns data for that specific character. Otherwise, the server will return an HTTP 404 error.

To update an existing character, update swagger.yml with this code:

# swagger.yml

# ...

paths:
  /people:
    # ...
  /people/{lname}:
    get:
        # ...
    put:
      tags:
        - People
      operationId: "people.update"
      summary: "Update a person"
      parameters:
        - $ref: "#/components/parameters/lname"
      responses:
        "200":
          description: "Successfully updated person"
      requestBody:
        content:
          application/json:
            schema:
              x-body-name: "person"
              $ref: "#/components/schemas/Person"

With this definition of the operation put your server is waiting update() V people.py:

# people.py

# ...

def update(lname, person):
    if lname in PEOPLE:
        PEOPLE[lname]["fname"] = person.get("fname", PEOPLE[lname]["fname"])
        PEOPLE[lname]["timestamp"] = get_timestamp()
        return PEOPLE[lname]
    else:
        abort(
            404,
            f"Person with last name {lname} not found"
        )

Function update() expects arguments lname And person. When a character with the specified last name exists, you update the corresponding values ​​in PEOPLE with the character's data.

To get rid of a character in your dataset, you need to work with the delete operation:

# swagger.yml

# ...

paths:
  /people:
    # ...
  /people/{lname}:
    get:
        # ...
    put:
        # ...
    delete:
      tags:
        - People
      operationId: "people.delete"
      summary: "Delete a person"
      parameters:
        - $ref: "#/components/parameters/lname"
      responses:
        "204":
          description: "Successfully deleted person"

Add the appropriate function delete() V person.py:

# people.py

from flask import abort, make_response

# ...

def delete(lname):
    if lname in PEOPLE:
        del PEOPLE[lname]
        return make_response(
            f"{lname} successfully deleted", 200
        )
    else:
        abort(
            404,
            f"Person with last name {lname} not found"
        )

If the character you want to remove exists in your dataset, then you remove the element from PEOPLE.

With all the character management endpoints in place, it's time to test your API. Since you used Connexion to connect your Flask project to Swagger, your API annotations will be available when you restart your server.

This user interface allows you to see all the documentation you included in the swagger.yml file and interact with all the URL endpoints that make up the CRUD functionality of the people interface.

At this time, any changes you make will not be saved when you restart your Flask application. This is why you will connect the database to your project in the next part.

Conclusion

In this part, you created a complex REST API using the Python Flask web framework. With the Connexion module and some additional configuration work, you can create useful online documentation. We hope that creating a REST API for a web application was not difficult.

In Part 1 you learned how to:

  • Create a basic Flask project using the REST API

  • Handle HTTP requests using Connexion

  • Define API endpoints using the OpenAPI specification

  • Interact with your API to manage data

  • Create API documentation using Swagger UI

In Part 2 of this series, you'll learn how to use a database to permanently store your data instead of relying on in-memory storage as you did here.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *