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 |
|
| Reading the Character List |
Create |
|
| Creating a new character |
Read |
|
| Retrieving character data |
Update |
|
| Updating an existing character |
Delete |
|
| 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:8000
you 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 url
you 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 endpointget:
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/people
which 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 requesttags:
Tags assigned to this endpoint that allow operations to be grouped in the user interfacesummary:
Annotation text in the UI for this endpointresponses:
Status codes that the endpoint responds withoperationId
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/people
your 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/ui
to 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 |
|
| Reading the Character List |
Create |
|
| Creating a new character |
Read |
|
| Retrieving character data |
Update |
|
| Updating an existing character |
Delete |
|
| 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.yml
add 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 required
must 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 operationId
which 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 /people
you 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.py
so 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.