Flask 4 - REST APIs met Connexion
Inleiding
We bouwden een eenvoudige Flask applicatie voor een blog website. De browser stuurt HTTP GET of POST requests naar de applicatie en deze voert de nodige acties uit en stuurt een HTML pagina terug.
Maar wat als we…
- Dezelfde dienst willen aanbieden als een mobile app?
- Willen integreren met andere diensten (bvb. om automatisch een blog post aan te maken)?
- Met een dynamisch en interactief frontend framework zoals React willen werken?
Voor die situaties is het handiger dat de blog “server” gestructureerde data, zoals JSON stuurt en ontvangt. De verschillende clients kunnen dan de data elk op hun eigen manier verwerken en presenteren.
REST API
De server wordt dan een API of Application Programming Interface, een verzameling afspraken (regels en functies) waarmee softwareonderdelen met elkaar kunnen praten. Die onderdelen kunnen lokaal binnen eenzelfde systeem of programma zijn – bvb. de verzameling standard functies en classes in Python. Maar die onderdelen kunnen ook aan een typisch client-server model voldoen, zoals een web-API waarmee blog posts kunnen worden opgehaald, aangemaakt of gewijzigd.
- Er is steeds een client die communiceert met een API server.
- De resources (in dit voorbeeld blogpost) hebben elk een unieke identificatie (1, 2, …).
- Elke request van een client wordt apart behandeld door de API server, zonder verband met vorige requests van dezelfde client. We zeggen dat de server stateless is.
Deze eigenschappen vormen de basis voor wat gekend is als REST, of REpresentational State Transer.
Doorgaans wordt als onderliggend protocol steeds HTTP gebruikt en is JSON het formaat gebruikt om data (de resources) uit te wisselen.
JSON
JSON, of JavaScript Object Notation is een manier om data op een gestructureerde en leesbare manier voor te stellen.
Onze blogpost uit onze Flask applicatie heeft bijvoorbeeld altijd een unieke ID, een titel en een inhoud.
{
"id": 1,
"title": "Hello World!",
"body": "Today I used Flask, and it was quite nice.\nI like it a lot."
}Python ondersteunt JSON rechtstreeks!
import json
post = {
"id": 1,
"title": "Hello World!",
"body": "Today I used Flask, and it was quite nice.\nI like it a lot.",
}
with open("post.json", mode="w", encoding="utf-8") as file:
1 json.dump(post, file)
with open("post.json", mode="r", encoding="utf-8") as file:
2 loaded_post = json.load(file)
print(type(loaded_post))
print(loaded_post)
# Output:
# <class 'dict'>
# {'id': 1, 'title': 'Hello World!', 'body': 'Today I used Flask, and it was quite nice.\nI like it a lot.'}- 1
-
Met
json.dumpkunnen we een basis Python type (dict,list, …) omzetten naar JSON en opslaan in een bestand. - 2
-
Omgekeerd kunnen we met
json.loadJSON data uit een bestand inlezen.
REST API met de Flask App
Wat als we onze bestaande Flask applicatie willen aanpassen naar een REST API?
- Maak een nieuwe
/postsroute, op basis van de bestaandeindexview. Bij REST APIs wordt veelal gesproken over een endpoint i.p.v. een route. - We gebruiken geen Jinja2 (dus geen
render_template). - Als een view een
listofdictreturnt zet Flask dit automatisch om in JSON.
Oplossing
We bekijken wat we precies teruggestuurd krijgen, met de browser developer tools, of bvb. met curl:
curl -v http://127.0.0.1:5000/posts
* Trying 127.0.0.1:5000...
* Connected to 127.0.0.1 (127.0.0.1) port 5000
> GET /posts HTTP/1.1
> Host: 127.0.0.1:5000
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: Werkzeug/3.1.3 Python/3.13.5
< Date: Sun, 28 Sep 2025 09:00:39 GMT
< Content-Type: application/json
< Content-Length: 191
< Vary: Cookie
< Connection: close
<
{
"posts": [
{
"author_id": 1,
"body": "world",
"created": "Tue, 23 Sep 2025 06:12:15 GMT",
"id": 1,
"title": "hello",
"username": "fab"
}
]
}OpenAPI
OAS
De OpenAPI Specification (OAS) is een standaard formaat om REST API’s te beschrijven. Het is eigenlijk een soort contract dat vastlegt:
- welke endpoints er zijn
- welke parameters en request bodies verwacht worden
- welke responses terugkomen
Met zo een contract weten gebruikers (clients) van de API precies hoe ze de deze moeten gebruiken en wat ze kunnen verwachten.
Dit gebeurt meestal in een YAML of JSON bestand. Zo’n beschrijving is zowel leesbaar voor mensen (documentatie) als bruikbaar voor machines (bijv. codegeneratie, validatie, testen).
Hieronder krijg je als voorbeeld een voorproefje van hoe de Flaskr blog app uit de vorige les er zou uitzien als een API, beschreven als een OpenAPI specification. We komen hier later in meer detail op terug.
openapi: 3.0.3
info:
title: Flaskr Blog API
version: 1.0.0
description: API for the Flaskr blog (create, read, update, delete posts)
servers:
- url: http://localhost:5000
description: Local development server
paths:
/posts:
get:
summary: List all posts
responses:
'200':
description: A list of posts
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
post:
summary: Create a new post
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PostInput'
responses:
'201':
description: Post created
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
/posts/{postId}:
parameters:
- name: postId
in: path
required: true
schema:
type: integer
get:
summary: Retrieve a specific post
responses:
'200':
description: The requested post
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
'404':
description: Post not found
put:
summary: Update an existing post
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PostInput'
responses:
'200':
description: Updated post
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
'404':
description: Post not found
delete:
summary: Delete a post
responses:
'204':
description: Post deleted (no content)
'404':
description: Post not found
components:
schemas:
Post:
type: object
properties:
id:
type: integer
title:
type: string
body:
type: string
required:
- id
- title
- body
PostInput:
type: object
properties:
title:
type: string
body:
type: string
required:
- title
- bodySwagger
De OpenAPI specification stond vroeger bekend als Swagger specification en is oorspronkelijk ontwikkeld door het bedrijf Smartbear. De specificatie is in tussen 2015 en 2017 afgesplitst als een afzonderlijk open initiatief. De term Swagger spec wordt nog veel gebruikt maar eigenlijk is het dus OAS. Meer informatie vind je op https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/.
Smartbear ontwikkelt wel nog een reeks tools onder de noemer Swagger waarvan we er twee aan bod laten komen.
Swagger UI laat via een interactieve web pagina toe OpenAPI specs te visualiseren en te testen. Een demo vind je op https://petstore.swagger.io/.
Swagger Editor is een online editor waar je een OAS in kan plakken en bewerken. Je krijgt live de bijhorende Swagger UI te zien. Probeer dit zeker uit met de Flask Blog spec die we hiervoor hebben bekeken.
Connexion Introductie
Wat is Connexion
Connexion is een web framework ontwikkeld rond 2015 door Zalando (ja, die Zalando), met als doel het ondersteunen van een API-first of Spec-first methodologie. Hierbij wordt een toekomstige API eerst volledige gedefinieerd met een OpenAPI spec. (Meer over API-first in deze blogpost van Zalando zelf, of deze van Atlassian)
Connexion bouwt boven op Flask en biedt als belangrijkste features:
- Het linken van route functies aan operations (path + method) in de OAS
- Automatische validatie van requests parameters en body (op basis van de OAS)
- Parameter parsing en injectie (geen
flask.requestobject nodig) - Automatisch correct omzetten van respons objecten
- Swagger UI integratie
- Authenticatie
Zoals we in dit hoofdstuk verder zullen zien wordt het met Connexion een stuk gemakkelijker een REST API te maken dan wanneer we alles zelf zouden moeten doen met enkel Flask.
Mocking
Connexion komt met een CLI waarmee je onder andere zonder extra code een OpenAPI spec beschikbaar kan maken via een test server. Dit kan bijzonder handig zijn om toekomstige API clients alvast de overeengekomen OAS te laten valideren.
Voorbeeld:
Quotes API
Intro
Als eerste kennismaking met Connexion bouwen we samen een eenvoudige API om bekende quotes te beheren. We starten met een enkel endpoint:
GET /quotes/random- stuurt een random quote terug
De OpenAPI spec ziet er als volgt uit:
quotes/openapi.yaml
openapi: 3.0.0
info:
title: Quotes API
version: 1.0.0
1paths:
/quotes/random:
2 get:
3 operationId: quotes.app.get_random_quote
4 summary: Get a random quote
5 responses:
"200":
description: A single random quote
content:
6 application/json:
schema:
$ref: "#/components/schemas/Quote"
7components:
8 schemas:
9 Quote:
type: object
properties:
id:
type: integer
text:
type: string
required: [id, text]- 1
-
Onder
pathskomen alle unieke paden. We starten met enkel/quotes/random. - 2
-
Onder elk pad komen één of meerdere HTTP methods die beschikbaar zijn voor dit pad. We starten met enkel
get. - 3
-
operationIdbepaald de view functie voor deze operation (= combinatie van path + method). In dit geval de functionget_random_quotein thequotes/app.pymodule. - 4
-
summarygeeft een korte beschrijving van wat deze operation doet. - 5
-
responsesbevat één of meerdere HTTP status codes. Voor elke code geeft dedescriptioneen korte beschrijving van wat je mag verwachten in dit geval, en decontentde exacte beschrijving van de structuur van de respons. - 6
-
In dit geval bevat de respons (body) JSON data. Het
schemavan die data kan ofwel rechtstreeks beschreven worden, ofwel verwijzen naar een latere schema definitie. In dit geval kiezen we voor de tweede optie omdat we het schema van “een quote” ook in andere operations zullen gebruiken. - 7
-
componentsis net zoalspathseen top-level element. Het is optioneel en kan verschillende componenten bevatten die gebruikt worden in de operations. - 8
-
In dit geval bevat
componentsenkelschemas. Elk schema beschrijft een element waar elders naar verwezen kan worden. - 9
-
We starten met een enkel schema met de (vrij te kiezen) naam
Quote. Een Quote is eenobject(kan je zien als het JSON equivalent van eendict) en bevat eeniden eentext. Voor elk element (“property”) in het object geven we het type mee (hierintenstring). Optioneel kunnen we ook definiëren welke elementen verplicht aanwezig moeten zijn, metrequired.
We starten een nieuw project en installeren Connexion:
mkdir quotes
cd quotes
uv init
uv add "connexion[flask,swagger-ui,uvicorn]~=3.0"mkdir quotes
cd quotes
conda create -n quotes
conda activate quotes
conda install "connexion[flask,swagger-ui,uvicorn]~=3.0"
conda activate quotesDe OpenAPI spec plaatsen we in een quotes folder, waar zo meteen ook de Python code komt te staan.
./quotes
├── quotes/
└── openapi.yamlquotes/app.py
import random
import connexion
QUOTES = [
{"id": 1, "text": "Premature optimization is the root of all evil. - Donald Knuth"},
{"id": 2, "text": "Talk is cheap. Show me the code. - Linus Torvalds"},
{"id": 3, "text": "Good code is its own best documentation. - Steve McConnell"},
{"id": 4, "text": "Make it work, make it right, make it fast. - Kent Beck"},
]
1def get_random_quote():
return random.choice(QUOTES)
2app = connexion.FlaskApp(__name__)
3app.add_api("openapi.yaml")
if __name__ == "__main__":
4 app.run("quotes.app:app", port=5000)- 1
-
get_random_quoteis the view functie en komt overeen met deoperationIdin the OpenAPI spec. De functie returnt een random gekozendict. Net als Flask zet Connexion dit automatisch om naar JSON in de HTTP respons, en gebruikt default status code 200. - 2
- Een Connexion app object maken werkt op dezelfde manier als bij Flask. Dit object stelt de volledige applicatie voor.
- 3
-
Met
add_apikan een OpenAPI spec ingeladen worden. Dit kan herhaald worden als er verschillende specs zijn. - 4
-
app.runstart een eenvoudige test server om te applicatie via HTTP toegankelijk te maken.
Testen en Swagger UI
Om de applicatie te starten:
curl is een bijzonder populaire all-around CLI HTTP client. Hiermee kunnen we eenvoudige requests naar onze API sturen.
$ curl http://127.0.0.1:5000/quotes/random
{
"id": 4,
"text": "Make it work, make it right, make it fast. - Kent Beck"
}We krijgen, zoals verwacht, een JSON respons met een van de voorgeprogrammeerde quotes.
Zoals eerder aangehaald bestaat er ook een zogenaamde Swagger UI, een webpagina waarmee de API bekeken en gebruikt kan worden. Omdat we Connexion geïnstalleerd hebben met de swagger-ui is deze UI automatisch beschikbaar via http://127.0.0.1:5000/ui.
Oefeningen
De volgende oefeningen voegen extra operaties toe.
Alle quotes opvragen
Met de GET /quotes operatie willen we de lijst met alle quotes terugkrijgen.
Om te testen gebruik je het volgende curl commando:
Oplossing
quotes/openapi.yaml
Een specifieke quote opvragen
Met GET /quotes/{id} willen we enkel de quote met de gegeven id terugkrijgen. Als de id niet bestaat willen we een HTTP 404 respons met een foutmelding.
Om te testen gebruik je het volgende curl commando:
- Een
pathmet een variabel deel wordt voorgesteld met/quotes/{id} - Die
idis dan een parameter. Parameters worden in eenparameterslijst (onder de method, dus op hetzelfde niveau alssummaryenresponses) gedefinieerd.
Documentatie: - https://spec.openapis.org/oas/latest.html#path-templating - https://spec.openapis.org/oas/latest.html#parameter-object
Deze id parameter definieer je als volgt:
Oplossing
quotes/openapi.yaml
paths:
/quotes/{id}:
get:
operationId: quotes.app.get_quote_by_id
summary: Get a quote by ID
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: The ID of the quote
responses:
"200":
description: A single quote
content:
application/json:
schema:
$ref: "#/components/schemas/Quote"
"404":
description: Quote not foundEen quote toevoegen
Met de POST /quotes operatie moet een nieuwe quote worden toegevoegd. Hiervoor voegen we gewoon een item toe aan de QUOTES lijst (die toegevoegde quotes gaan dus verloren als de server herstart). In de request verwachten we een JSON object met enkel het text veld. In de respons willen we de volledige quote die is toegevoegd, inclusief de id.
Om te testen gebruik je het volgende curl commando:
$ curl -X 'POST' 'http://127.0.0.1:5000/quotes'-H 'Content-Type: application/json' -d '{"text": "Python is an experiment in how much freedom programmers need. - Guido van Rossum"}'- Een JSON object in de request body definieer je
requestBody, onder de method, dus op hetzelfde niveau alssummaryenresponses.
Documentatie: https://spec.openapis.org/oas/latest.html#request-body-object
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewQuote"NewQuote is dus een extra schema dat we ook moeten definiëren. Het is eigenlijk hetzelfde als de bestaande Quote, maar zonder de id.
Oplossing
quotes/openapi.yaml
paths:
post:
operationId: quotes.app.add_quote
summary: Add a new quote
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewQuote"
responses:
"201":
description: Quote created
content:
application/json:
schema:
$ref: "#/components/schemas/Quote"
schemas:
NewQuote:
type: object
properties:
text:
type: string
required:
- textWe kunnen nu ook demonstreren hoe Connexion automatisch de request data voor ons valideert:
$ curl -X 'POST' 'http://127.0.0.1:5000/quotes' -H 'Content-Type: application/json' -d '{"text": 123}'
{"type": "about:blank", "title": "Bad Request", "detail": "123 is not of type 'string' - 'text'", "status": 400}
$ curl -X 'POST' 'http://127.0.0.1:5000/quotes' -H 'Content-Type: application/json' -d '{"txt": "Test"}'
{"type": "about:blank", "title": "Bad Request", "detail": "'text' is a required property", "status": 400
$ curl -X 'POST' 'http://127.0.0.1:5000/quotes' -H 'Content-Type: text/plain' -d '{"text": "test"}'
{"type": "about:blank", "title": "Unsupported Media Type", "detail": "Invalid Content-type (text/plain), expected ['application/json']", "status": 415}Authenticatie
Connexion voorziet verschillende opties om een API request de authentificeren. We bekijken hier als voorbeeld een eenvoudige HTTP Basic Authentication. In de Connextion documentatie vind je alle andere mogelijkheden.
In de OAS zijn 2 toevoegingen nodig:
quotes/openapi.yaml
- 1
-
In elke operation waarvoor we authenticatie willen gebruiken voegen we een
securityblock toe dat verwijst naar eensecurityScheme. In dit geval willen we enkel de posten van een nieuwe quote beveiligen. - 2
-
Onder
componentsvoegen wesecuritySchemestoe. We definiëren hier maar een schema met de naambasic(moet overeenstemmen met de naam gebruikt in stap 1). Dex-basicInfoFuncparameter verwijst naar de Python functie die de authenticatie zal uitvoeren.
quotes/app.py
Dit is natuurlijk een simpel en onveilig voorbeeld. In de praktijk zou bijvoorbeeld een password-database gebruikt worden, met enkel een hash van de wachtwoorden.
Een test, zonder en dan met authenticatie:
$ curl -X 'POST' -H 'Content-Type: application/json' 'http://127.0.0.1:5000/quotes' -d '{"text": "test"}'
{"type": "about:blank", "title": "Unauthorized", "detail": "No authorization token provided", "status": 401}
$ curl -X 'POST' -u admin:secret -H 'Content-Type: application/json' 'http://127.0.0.1:5000/quotes' -d '{"text": "test"}'
# Of:
$ curl -X 'POST' -H 'Authorization: Basic YWRtaW46c2VjcmV0' -H 'Content-Type: application/json' 'http://127.0.0.1:5000/quotes' -d '{"text": "test"}'
{
"id": 5,
"text": "test"
}YWRtaW46c2VjcmV0 is de Base64 encoding van de string admin:secret.
Flaskr-API
In dit hoofdstuk bouwen we onze Flaskr blog website om naar een REST API. We bouwen hier verder op de implementatie met SQLAlchemy, uit les 3. Heb je geen werkende code, dan kan je gebruik maken van https://github.com/dvx76/flask-tutorial/tree/sqlalchemy.
Tip: maak een nieuw Git branch in je flaskr project.
Voor we kunnen starten moeten we natuurlijk het Connexion package installeren.
Blog-post Representatie
Eerste moeten we definiëren hoe we een blog-post gaan voorstellen in JSON. Één blog-post bestaat uit een aantal velden, waardoor het in JSON de type object zal krijgen. Voor elke blog-post hebben we deze velden:
idis eenintegeren bevat de unieke ID van de postauthoris eenstringen bevat de naam van de auteurcreatedis een timestamp (date) maar zal in JSON voorgesteld worden als eenstringtitleis een stringbodyis een string
Op basis hiervan kunnen we onze OpenAPI Spec starten met een schema voor een post.
Het is zeker niet noodzakelijk om object schemas in de OpenAPI Spec te laten overeenstemmen met het database schema van de applicatie.
Bij het opmaken van de OAS bepaal je op welke manier je informatie will representeren voor de gebruikers van de API. Zo kiezen we er hier voor om de username van de post auteur terug te sturen, en niet de user_id. We combineren en selecteren dus data uit 2 verschillende database tabellen.
Het is zelfs aangeraden om de API schemas volledig los te koppelen van interne representaties (zoals een database schema). Eenmaal in gebruik is een API immers moeilijk aan te passen. Maar we willen mogelijks wel de interne representatie aanpassen.
flaskr/openapi.yaml
openapi: 3.0.3
info:
title: Flaskr Blog API
version: 1.0.0
description: API for the Flaskr blog
components:
schemas:
Post:
type: object
properties:
id:
type: integer
author:
type: string
1 created:
type: string
format: date-time
title:
type: string
body:
type: string- 1
-
De
createdtimestamp is dus eenstringtype maar metformat: date-timemaken we duidelijk dat deze string een timestamp voorstelt.
Eerste operatie: GET all posts
Om te starten implementeren we GET /posts. We willen een lijst terugkrijgen met alle bestaande posts.
Een eerste path in de OAS:
flaskr/openapi.yaml
Voor de implementatie in flaskr/app.py kunnen we ons grotendeels baseren op het vorig quotes project en index view uit onze Flask code.
We starten met de volgende code en werken samen als oefening de get_all_posts functie af:
flaskr/app.py
import connexion
from sqlalchemy import select
from sqlalchemy.orm import joinedload
from .db import create_db_session
from .models import Post
app = connexion.FlaskApp(__name__)
app.add_api("openapi.yaml")
1db_session, remove_session = create_db_session(None)
2app.app.teardown_appcontext(remove_session)
def get_all_posts():
# Verder af te werken
if __name__ == "__main__":
app.run("flaskr.app:app", port=5000)- 1
-
Om een SQLAlchemy Session te verkrijgen kunnen we de bestaande functie uit de
dbmodule gebruiken. We laten de functie de defaultdatabase_urlgebruiken (= SQLite). Voor de eenvoud en omdat we hier uit een enkel bestand werken gebruiken wedb_sessionals globale variabele en hoeven we in de route functies geen gebruik de maken vanget_db_session. - 2
-
appis nu eenconnexion.FlaskAppobject, en geenflask.Flaskobject. Via.apphebben we wel nog toegang tot het onderliggendeFlaskobject. Op die manier kunnen we alsnog deteardown_appcontextinstellen.
Oplossing
flaskr/app.py
import connexion
from sqlalchemy import select
from sqlalchemy.orm import joinedload
from .db import create_db_session
from .models import Post
app = connexion.FlaskApp(__name__)
app.add_api("openapi.yaml")
db_session, remove_session = create_db_session(None)
app.app.teardown_appcontext(remove_session)
def get_all_posts():
posts = db_session.scalars(
1 select(Post).options(joinedload(Post.author)).order_by(Post.created.desc())
)
return [
2 {
"id": post.id,
"author": post.author.username,
"created": post.created,
"title": post.title,
"body": post.body,
}
for post in posts
]
if __name__ == "__main__":
app.run("flaskr.app:app", port=5000)- 1
-
We gebruiken
joinedloadomdat we voor elkePostook deauthor.usernamewillen gebruiken. - 2
- We zetten “handmatig” het resultaat van de ORM query om naar de gewenste API respons. Connexion zorgt voor het serializeren naar JSON.
Een blog-post toevoegen
Dit wordt de POST /posts operation. Om een nieuwe blog-post te maken hebben we enkel title en body nodig. We voegen hiervoor een extra schema toe in de OAS.
flaskr/openapi.yaml
paths:
/posts:
post:
summary: Create a new post
operationId: flaskr.app.create_post
requestBody:
1 required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewPost"
responses:
2 "201":
description: Post created
content:
application/json:
schema:
3 $ref: "#/components/schemas/Post"
components:
schemas:
NewPost:
type: object
properties:
title:
type: string
body:
type: string
required:
- title
- body- 1
-
In de
POSTrequest verwachten we een body in JSON formaat volgens hetNewPostschema. - 2
- HTTP 201 - Created, we bevestigen aan de client dat de post resource is aangemaakt.
- 3
-
Bij REST APIs is het gebruikelijk om bij een
POSTalle details van de aangemaakte resource terug te sturen (m.a.w. hetzelfde antwoord als bij eenGETvoor die specifieke resource)
Voor de bijhorende functie starten we opnieuw vanuit de overeenkomstige Flask view.
flaskr/app.py
- 1
-
Connexion valideert automatisch de JSON body van de request, en zet deze om naar een passend Python type - in dit geval een
dict. - 2
-
We hebben een
author_id(ofauthor) nodig maar hebben (nog) geen authenticatie ingebouwd. Als tijdelijke oplossing hardcoden we de id van een bestaande user. - 3
-
Eenmaal het
Postobject gecommit is naar de database zal SQLAlchemy ORM automatisch het object in de session updaten, bvb met deid.
Om de nieuwe operation te testen gebruiken we het volgende curl commando:
Oefeningen
De volgende oefeningen voegen extra operaties toe.
Specifieke blog-post opvragen
Dit wordt de GET /posts/{id} operatie. Bedenkt wat er moet gebeuren als de gevraagde post niet bestaat.
Oplossing
flaskr/openapi.yaml
paths:
/posts/{id}:
get:
summary: Get a single post
operationId: flaskr.app.get_post
parameters:
- in: path
name: id
required: true
schema:
type: integer
responses:
"200":
description: A single post
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
"404":
description: Post not foundflaskr/app.py
def get_post(id: int):
post = db_session.scalar(
select(Post).options(joinedload(Post.author)).where(Post.id == id)
)
if not post:
return {"message": f"Post {id} not found"}, 404
return {
"id": post.id,
"author": post.author.username,
"created": post.created,
"title": post.title,
"body": post.body,
}Blog-post aanpassen
Met PUT /posts/{id}.
Oplossing
flaskr/openapi.yaml
/posts/{id}:
put:
summary: Update a post
operationId: flaskr.app.update_post
parameters:
- in: path
name: id
required: true
schema: {type: integer}
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewPost"
responses:
"200":
description: Updated post
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
"400":
description: Invalid input
"404":
description: Post not foundflaskr/app.py
def update_post(id: int, body: dict):
post = db_session.scalar(
select(Post).options(joinedload(Post.author)).where(Post.id == id)
)
if not post:
return {"message": f"Post {id} not found"}, 404
post.title = body["title"]
post.body = body["body"]
db_session.commit()
return {
"id": post.id,
"author": post.author.username,
"created": post.created,
"title": post.title,
"body": post.body,
}Blog-post verwijderen
Met DELETE /posts/{id}.
Oplossing
flaskr/openapi.yaml
Refactoring
Het is je waarschijnlijk opgevallen dat in de meeste operation functies dezelfde code wordt herhaald om een Post ORM object om te zetten naar een dict.
We zouden een eenvoudige helper functie kunnen toevoegen, bvb.
Authenticatie
Dit kan opnieuw met HTTP Basic Auth, zoals in de Quotes API. Deze keer hebben we wel een echte user database waar we het password mee kunnen valideren. Dat kan op dezelfde manier gebeuren als in de Flask implementatie.
Op deze manier willen we de POST operatie aanpassen om de juiste author_id te gebruiken, en de PUT en DELETE operaties beveiligen zodat een gebruiken enkel zijn eigen posts kan aanpassen.
Bedenkt ook eens wat er moet gebeuren als de author_id niet overeenkomt.
Voor de OAS volgen we precies dezelfde aanpak als bij de Quotes API.
flaskr/openapi.yaml
flaskr/app.py
def auth(username: str, password: str):
user = db_session.scalar(select(User).where(User.username == username))
if user and check_password_hash(user.password, password):
1 return {"sub": user.id, "username": username}
return- 1
-
De return value (
dict) van deze functie is automatisch beschikbaar in alle view functies via een extratoken_infoargument.
PUT en DELETE
Voor de PUT en DELETE operations kunnen we simpelweg de post.author_id vergelijken met de userid uit de authenticatie. Die laatste is beschikbaar via token_info["sub"].
Als deze niet overeenkomt krijgt de client een 404/Not Found respons. Dit is in de praktijk niet ongebruikelijk omdat op deze manier het al dan niet bestaan van de gevraagde resource ID niet wordt vrijgegeven (ook al is dat hier minder relevant omdat je zonder authenticatie alle posts kan ophalen).
flaskr/app.py
def update_post(id: int, body: dict, token_info: dict):
post = db_session.scalar(
select(Post).options(joinedload(Post.author)).where(Post.id == id)
)
if not post or post.author_id != token_info["sub"]:
return {"message": f"Post {id} not found"}, 404
post.title = body["title"]
post.body = body["body"]
db_session.commit()
return _post_to_dict(post)
def delete_post(id: int, token_info: dict):
post = db_session.scalar(
select(Post).options(joinedload(Post.author)).where(Post.id == id)
)
if not post or post.author_id != token_info["sub"]:
return {"message": f"Post {id} not found"}, 404
db_session.delete(post)
db_session.commit()
return None, 204POST
Voor het maken van een nieuwe post gebruiken we dezelfde aanpak. token_info["sub"] vervangt de tot nu toe hardcoded waarde 1.
Extra
We halen kort nog een aantal onderwerpen aan voor wie OpenAPI en Connexion nog verder wil verkennen
Petstore
Wil je een uitgebreider voorbeeld van een OpenAPI spec, bekijk dan zeker eens Petstore en de bijhorende specificatie. Dit is een beetje het defacto voorbeeld dat door de jaren en versies heen is mee geëvolueerd met de specificatie.
Testen
Net als Flask voorziet Connexion een test client. Een eenvoudig voorbeeld:
Context
Ook zoals Flask heeft Connexion verschillende context objecten, zoals bvb connexion.request, waarmee je rechtstreeks toegang krijgt tot de request details (headers, path, query params, …)
Exception Handlers
In de huidige implementatie proberen we fouten, zoals wanneer een gevraagde post niet bestaat, rechtstreeks af te handelen door een aangepaste status code and body terug te sturen als respons.
Voor dit eenvoudig voorbeeld is dat prima maar in complexere applicaties wordt de code al snel opgesplitst in verschillende modules en wordt er vaak met (eigen) exceptions gewerkt.
Op https://connexion.readthedocs.io/en/stable/exceptions.html vinden we niet alleen een overzicht van de verschillende exceptions eigen aan Connexion, en hoe deze worden afgehandeld, maar ook een manier om andere exceptions af te handelen.
We laten hier een mogelijke aanpak zien waarbij we een eigen exception maken die we zullen raisen wanneer een post niet gevonden is.
flaskr/app.py
class ResourceNotFound(connexion.ProblemException):
def __init__(self, resource_id: int | None = None):
super().__init__(
status=404,
title="NotFound",
detail=f"Resource {resource_id if resource_id else ''} does not exist",
)
def get_post(id: int):
post = db_session.scalar(
select(Post).options(joinedload(Post.author)).where(Post.id == id)
)
if not post:
raise ResourceNotFound(id)
return _post_to_dict(post)Referentie Implementatie
Een volledig werkende implementatie gebaseerd op deze les vind je op https://github.com/dvx76/flask-tutorial/tree/flaskr-api-sqla.
