Créer des API REST avec le framework FastAPI en Python
- 2021-03-03
- Publié par : Christophe DELEUZE
- Catégorie : FastAPI
Le développement d’API est un vaste domaine et son écosystème est florissant. Sans surprise, Python est un acteur majeur dans cet espace Cela signifie également qu’il existe de nombreux frameworks conçus ou prenant en charge le développement d’API en Python. Il y a cependant trois noms qui semblent dominer : Flask, Django et FastAPI.
Flask et Django sont tous deux des frameworks de développement Web généraux, et ils sont excellents dans ce qu’ils font, développement d’API inclus.
FastAPI, quant à lui, est un projet plus petit, axé uniquement sur le développement d’API. FastAPI excelle tout simplement dans ce domaine. Bien que Flask et Django soient bons, il est difficile de plaider en leur faveur par rapport à FastAPI et nous allons voir pourquoi.
Donc, cet article présentera FastAPI. Nous verrons pourquoi ce framework est si particulier et comment nous pouvons jouer avec.
Pourquoi FastAPI ?
Il y a cinq grandes raisons qui font de FastAPI un nouvel acteur incontournable pour construire des API en python :
- Prise en charge asynchrone native
- Vitesses ultra-rapides
- Facilité d’utilisation
- Documentation automatisée
- Validation de données
Prise en charge asynchrone native
Le fait que FastAPI prenne en charge nativement les appels asynchrones, permet de paralléliser des requêtes à l’API.
Par exemple, si plusieurs requêtes sont envoyées à notre API, il est très intéressant de pouvoir les exécuter en parallèle afin d’améliorer le temps de réponse global de l’API. C’est exactement ce que nous pouvons faire avec FastAPI.
FastAPI est rapide à tous points de vue, mais je me réfère ici aux vitesses de latence.
C’est facilement l’option la plus rapide que nous ayons pour le développement d’API en Python.
Nous pouvons le comparer à d’autres frameworks populaires en utilisant TechEmpower, les résultats ressemblent à ceci :
Vitesses ultra-rapides
On remarque qu’entre Flask et FastAPI, au moment où j’écris cet article (mars 2021), nous avons une différence de vitesse ~7x, ce qui est appréciable.
Facilité d'utilisation
Concernant la facilité d’utilisation, FastAPI n’a rien à envier à Flask qui était déjà très facile dans son utilisation.
Vous verrez à quel point ce framework est facile à utiliser tout au long de cet article, mais pour un aperçu rapide, nous codons une requête GET comme ceci :
from fastapi import FastAPI
app = FastAPI()
@app.get()
def ma_fonction():
...
return reponse
Documentation automatisée
L’une des fonctionnalités que je préfère avec FastAPI est sa documentation qui est automatiquement générée lorsque nous déployons notre API.
Nous le couvrirons un peu tout au long de cet article, mais pour résumer, toute méthode contenue par notre API sera identifiée et utilisée pour produire une documentation propre et détaillée à l’aide de Swagger UI.
Validation de données
Enfin, le dernier point dont je souhaite souligner la force, pour FastAPI, concerne sa gestion native de la validation des données en entrée de l’API grâce à Pydantic et Typing.
Cet aspect est très important, car il permet d’augmenter le niveau qualitatif de l’API et d’apporter de la robustesse en sécurisant votre API.
Construire une API
Nous allons commencer par créer un fichier de script nommé main.py, évidemment ce nom de script peut être changé.
Dans ce script, nous allons créer une API simple avec trois méthodes : GET, POST et DELETE.
N’hésitez pas à vous référer au script complet à la fin de cette section.
Installation et initialisation
Les API construites avec FastAPI sont très faciles à configurer. La première chose à faire est d’installer le framework dans votre environnement virtuel à l’aide de la commande pip install fastapi.
Une fois fait, nous pouvons commencer à coder. Ici, nous importons FastAPI et initialisons notre API comme ceci :
from fastapi import FastAPI
app = FastAPI()
Implémenter notre première méthode : GET
Pour ajouter une méthode de requête GET, nous décorons simplement notre fonction get_lieux() avec @app.get(), ce qui signifie que nous plaçons cela sur la ligne au-dessus de la fonction que nous aimerions exécuter chaque fois que nous allons envoyer une requête GET à notre API.
Si notre API contient plusieurs points de terminaison, nous définissons quel point de terminaison utiliser avec @app.get("/point-de-terminaison").
Nous n’utiliserons pas plusieurs points de terminaison dans cet exemple, mais nous définirons un seul point de terminaison /lieux pour voir comment cela fonctionne, ce n’est cependant pas obligatoire !
Donc, nous voulons que notre requête GET renvoie tous les lieux contenus dans un ensemble de données (que nous définirons ensuite). Pour ce faire, nous écrivons :
@app.get("/lieux")
def get_lieux():
return {'donnees': donnees}, 200
Comme vous l’aurez remarqué, c’est très simple ! La seule inconnue ici est la variable donnees. En règle générale, nous utiliserions quelque chose comme une base de données SQL connectée à l’API, mais par souci de simplicité, nous utiliserons ici un dictionnaire défini dans main.py :
donnees = {
'lieux': [
'Paris',
'Lyon',
'Marseille',
'Montpellier',
'Toulon',
'Lilles',
'Nantes']
}
Il y a une dernière chose que nous devons faire pour terminer notre méthode GET. Pour autoriser les opérations asynchrones, nous ajoutons simplement async à notre définition de fonction :
@app.get("/lieux")
async def get_lieux():
return {'donnees': donnees}, 200
D’accord, nous avons initialisé notre API, défini nos données et ajouté la méthode GET. Comment tout cela s’intègre-t-il ?
from fastapi import FastAPI
app = FastAPI()
donnees = {
'lieux': [
'Paris',
'Lyon',
'Marseille',
'Montpellier',
'Toulon',
'Lilles',
'Nantes']
}
@app.get("/lieux")
async def get_lieux():
# renvoyer nos données et 200 code OK
return {'donnees': donnees}
Et c’est tout, nous avons une API fonctionnelle, quoiqu’incomplète.
Déployer l'API avec uvicorn
Nous pouvons tester notre API en passant par une CLI (comme le CMD sous Windows) ou en déployant notre API avec uvicorn.
Pour ce faire, nous installons d’abord la bibliothèque avec pip install uvicorn. Une fois installée, nous tapons uvicorn main:app --reload à partir du répertoire qui contient main.py.
Nous devrions voir quelque chose comme ceci dans le terminal :
Maintenant, si nous basculons vers notre navigateur et naviguons vers https://127.0.0.1:8000/lieux, nous devrions voir notre réponse JSON contenant le dictionnaire donnees :
Désormais, à chaque fois que nous voulons tester notre API, nous pouvons la déployer en utilisant uvicorn main:app --reload.
Vous avez sans doute remarqué l’option --reload, celle-ci permet d’automatiquement recharger le serveur à chaque fois que vous modifiiez le code de votre API.
Ainsi, durant le processus de développement, pas besoin de relancer soi-même le serveur, il s’actualisera automatiquement à chacune de vos modifications.
Tester notre API
Maintenant que nous avons créé une API, intéressons-nous sur comment la tester. Nous commencerons par tester la méthode GET que nous avons déjà implémenté, puis nous ajouterons une méthode POST et DELETE.
Tout au long de ce didacticiel, nous utiliserons l’interface Swagger UI livrée avec FastAPI afin de nous permettre de tester notre API.
On pourrait utiliser des outils externes pour tester l’API tel que Postman ou autre, mais prendre l’habitude d’interagir avec Swagger UI directement, c’est déjà mettre un pied dans la documentation.
Swagger UI est très facile à utiliser. Une fois votre serveur uvicorn lancé, il suffit d’aller au point de terminaison /docs qui correspond pour nous à http://127.0.0.1:8000/docs.
Tester la méthode GET avec Swagger
Précédemment, nous avons implémenté une méthode GET. Celle-ci apparait donc de la façon suivante dans l’interface Swagger :
L’interface Swagger UI détaille chaque requête, que nous avons codé dans main.py, dans un onglet qui leur est dédié.
Donc si nous déployons l’onglet qui concerne notre précédente requête GET, il suffira de cliquer sur le bouton try it out, puis sur le bouton execute, pour exécuter la requête et lire la réponse dans l’encadrer correspondant :
Implémenter la méthode POST
La prochaine étape que nous allons détailler concerne la méthode de publication POST. Elle nous permet d’ajouter des enregistrements à nos données, qui dans notre cas seront de nouveaux lieux que nous allons ajouter à notre dictionnaire donnees.
Le décorateur que nous utiliserons est @app.post().
Ici, nous devrons transmettre des données à notre API, sinon, elle ne pourra pas savoir quel lieu nous aimerions ajouter à nos données de lieux.
Nous gérons cela dans notre API en ajoutant le paramètre lieu à notre définition de fonction, ce qui nous donne :
@app.post("/lieux")
def post_lieux(lieu: str):
...
Vous noterez la présence de :str qui complète le paramètre lieu. Cet ajout sert pour la validation de données.
Dans notre cas, comme un lieu correspond à une chaine de caractère, le paramètre qui sera fourni à la requête ne pourra-être qu’une chaine de caractères et rien d’autre.
Il ne nous reste plus qu’à créer la logique dans notre méthode POST qui va consister à ajouter un lieu à donnees['lieux']. Cependant, il faut préalablement vérifier que l’emplacement n’existe pas déjà.
@app.post("/lieux")
async def post_lieu(lieu: str):
# si le lieu est déjà présent, nous ne l'ajoutons pas aux données
if lieu in donnees['lieux']:
# donc retourner simplement la réponse avec un message disant qu'il existe déjà
return {'donnees': donnees, 'message': "l'emplacement existe déjà"}
# par contre s'il n'est pas présent, nous ajoutons le lieu aux données
else:
donnees['lieux'].append(lieu)
# réponse de retour
return {'donnees': donnees, 'message': "l'emplacement a été ajouté"}
Tout d’abord, nous vérifions si l’emplacement existe déjà dans donnees['lieux']. Si c’est le cas, nous répondons avec un code 200 OK et un message expliquant que l’emplacement existe déjà.
Si l’emplacement n’existe pas déjà, nous l’ajoutons simplement à donnees['lieux'] et répondons avec 200 OK et un message confirmant que l’emplacement a été ajouté !
Tester la méthode POST avec Swagger
Pour tester notre méthode POST, il nous suffit de retourner sur l’interface Swagger UI et de l’actualiser.
Nous y voyons maintenant la présence à la fois de la méthode GET et POST.
En cliquant sur la méthode POST puis sur Try it out, nous pouvons maintenant renseigner le champ lieu avec un nom de lieu.
Enfin, en cliquant sur execute, on remarque que notre dictionnaire donnees["lieux"] a bien été enrichi du lieu qu’on avait renseigné.
On peut le vérifier aussi en exécutant la requête GET qui retournera le dictionnaire à jour.
Implémenter la méthode DELETE
Notre dernière méthode à implémenter est la méthode de suppression DELETE. Nous passerons une valeur au paramètre lieu comme nous l’avons fait pour POST.
Pour définir la méthode DELETE, nous utilisons exactement le même modèle que celui utilisé pour la méthode POST :
@app.delete("/lieux")
def delete_lieu(lieu: str):
...
Notez que si nous avions souhaité avoir un point de terminaison dynamique reprenant le nom du lieu, il nous suffit de le préciser dans la chaine de caractère du point de terminaison de la façon suivante :
@app.delete("/lieux/{lieu}")
def delete_lieu(lieu: str):
...
La logique de notre méthode DELETE doit nous permettre de supprimer un lieu s’il existe dans donnees['lieux'].
@app.delete("/lieux")
async def delete_lieu(lieu: str):
# si le lieu est présent, supprimez-le
if lieu in donnees['lieux']:
donnees['lieux'].remove(lieu)
# réponse de retour confirmant la suppression
return {'data': donnees, 'message':'le lieu est supprimé'}
# s'il n'est pas présent, renvoyez simplement la réponse
else:
return {'data': donnees, 'message': "le lieu n'existe pas"}
Dans le cas où le lieu n’existe pas, nous pouvons simplement renvoyer 200 OK avec un message indiquant à l’utilisateur que le lieu n’existe pas (dans tous les cas, le résultat est le même).
Tester la méthode DELETE avec Swagger
Pour tester notre méthode DELETE, il nous suffit de retourner sur l’interface Swagger et de l’actualiser.
Nous y voyons maintenant la présence à la fois de la méthode GET, POST et DELETE.
En cliquant sur la méthode DELETE puis sur Try it out, nous pouvons maintenant renseigner le champ lieu avec un nom de lieu à supprimer.
Enfin, en cliquant sur Execute, on reçoit une réponse 200 OK que le lieu renseigné a bien été supprimé de notre dictionnaire donnees["lieux"]
Le mot de la fin
Nous avons terminé avec cette brève introduction à FastAPI, aussi voici l’ensemble du code que vous pouvez récupérer si vous souhaitez tester vous-même ce tutoriel :
from fastapi import FastAPI
app = FastAPI()
donnees = {
'lieux': [
'Paris',
'Lyon',
'Marseille',
'Montpellier',
'Toulon',
'Lilles',
'Nantes']
}
@app.get ("/lieux")
async def get_lieux():
# renvoyer nos données et 200 code OK
return {'donnees': donnees}
@app.post("/lieux")
async def post_lieu(lieu: str):
# si le lieu est déjà présent, nous ne l'ajoutons pas aux données
if lieu in donnees['lieux']:
# donc retourner simplement la réponse avec un message disant qu'il existe déjà
return {'donnees': donnees, 'message': "l'emplacement existe déjà"}
# par contre s'il n'est pas présent, nous ajoutons le lieu aux données
else:
donnees['lieux'].append(lieu)
# réponse de retour
return {'donnees': donnees, 'message': "l'emplacement a été ajouté"}
@app.delete("/lieux")
async def delete_lieu(lieu: str):
# si le lieu est présent, nous le supprimons
if lieu in donnees['lieux']:
donnees['lieux'].remove(lieu)
# confirmer la suppression
return {'donnees': donnees, 'message':'le lieu est supprimé'}
# s'il n'est pas présent, renvoyez simplement la réponse
else:
return {'donnees': donnees, 'message': "le lieu n'existe pas"}
Enfin, pour aller plus loin avec FastAPI, n’hésitez pas lire tous les articles associés que nous avons écrit sur ce blog.
Et si vous avez des questions, les commentaires sont là pour ça.
Très bon didacticiel, rapide et efficace!
Merci 😉