Dans ce tutoriel, nous allons apprendre pas à pas comment créer une API REST de zéro en Python avec le framework Flask. Cette API va vous permettre de créer un système de gestion d’utilisateur très simplifiée, pour mieux comprendre les mécaniques et la syntaxe de Flask.

Nous n’utiliserons pas de base de données, toutes les données seront gérées en Python, pour simplifier le tutoriel.

Prérequis

Afin de bien pouvoir suivre et comprendre la suite du contenu, il est d’abord nécessaire de :

  • Connaitre les bases de Python
  • Comprendre les bases du web (serveur web, requêtes http,…)
  • Savoir ce qu’est une API REST

Pour suivre ce tutoriel dans les meilleurs conditions, vérifiez également que Python est installé sur votre machine avec une version ≥ 3.11.1

Installation

Flask est un framework web pour Python, il est possible de s’en servir pour créer des sites/applications web, des APIs, et globalement tout ce qui nécessite d’écouter des requêtes HTTP et de pouvoir y répondre.

D’abord, nous allons installer Flask, grâce au gestionnaire de paquet pip :

$> pip install Flask

Une fois terminé, créez un dossier et donnez-lui un nom, pour accueillir notre projet d’API. Par exemple : mon-api-flask

Ma première API

Dans votre dossier projet, créez un fichier main.py, et collez-y le code suivant :

from flask import Flask

app = Flask(__name__)

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

Voilà ce que fait ce code :

  1. On initialise l’app Flask, en lui donnant le nom du module (ici la variable __ name __ contient « main » car le nom de notre fichier est main.py)
  2. On déclare une nouvelle route à la racine (/), sur laquelle on va écouter toutes les requêtes HTTP GET (par défaut) sur l’url définie
  3. On défini la logique du contrôleur de notre route, comme une fonction classique
  4. On retourne des informations (ici une chaîne de caractères) dans la réponse HTTP, qui sera accompagné d’un code 200 (par défaut)

Pour tester cet exemple, il faut lancer notre API, en passant le nom de notre application, avec la commande run :

$> flask --app main run

Vous devriez avoir un message disant : Serving Flask app 'main' qui signifie que votre API est en cours d’exécution !

Pour la tester, il suffit d’ouvrir l’url http://127.0.0.1:5000 dans votre navigateur, et si vous voyez s’incrire « Hello World », alors la première étape est réussie !

Renvoyer du JSON

Il y a de fortes chances pour que vous ayez envie que votre API ne renvoie pas de simples textes, mais puisse notamment renvoyer des objets au format JSON !

Et ça tombe bien, parce que Flask nous facilite grandement la tâche :

@app.route("/json")
def hello_json():
    return {"str1": "Hello", "str2": "world"}
		# ou return ["hello", "world"]

Et le tour est joué ! Les dictionnaires “clés-valeurs” (ainsi que les listes) sont automatiquement sérialisés en JSON par Flask.

Renvoyer des objets (classes)

Là où le code se complique légèrement, c’est lorsque l’on veut retourner de vrais objets (comprendre des « instances de classes »).

Et pourtant, c’est essentiel, car c’est une bonne pratique quand on créé une API!

Cela permet :

  • De faire de la validation de données (et de garder certains informations cachées)
  • De garder le même modèle de données que dans le reste de son application
  • De ne pas à avoir à repasser par un dictionnaire de clé-valeur pour chaque objet

Prenons une classe fictive simple :

class User:
	username: str
	firstname: str
	lastname: str
	password: str
	def __init__(self, username: str, firstname: str, lastname: str):
		self.username = username
		self.firstname = firstname
		self.lastname = lastname
		self.password = "azejbnzdfunIJIN65"

On ne peut pas la transformer directement ses instances en JSON, car Python ne sait pas comment faire à partir d’un objet complexe.

On va donc utiliser une bibliothèque de sérialisation/déserialisation et de validation de données appelée Marshmallow

$> pip install marshmallow

Et on va créer un validateur pour notre classe User, de manière à ce que marshmallow comprenne comment passer de notre classe User à un objet JSON, et vice-versa :

from marshmallow import Schema, fields

# [...]

class UserSchema(Schema):
    username = fields.Str(required=True)
    firstname = fields.Str(required=True)
    lastname = fields.Str(required=True)

# On créé le validateur à partir de notre schéma
user_validator = UserSchema()

Vous aurez remarqué que l’attribut password n’est pas présent, car on ne veut jamais le retourner en dehors de notre API… C’est un garde-fou !

Ajoutons maintenant une nouvelle route pour retourner notre utilisateur, créé grâce à notre classe User, et transformé par marshmallow :

users = []
users.append(User("JohnDoe", "John", "Doe"))

# [...]

# description: retourne le premier utilisateur de la liste
@app.route("/user")
def get_user():
    first_user = users[0]
		# dump(...) permet de transformer une instance de classe en json
    response = user_validator.dump(first_user)
    return response

Ouvrez http://127.0.0.1:5000/users et admirez le résultat : votre utilisateur, en bonne et due forme (et sans mot de passe) !

Gérer les différentes méthodes HTTP

Une API, ça ne sert pas simplement à récupérer des données, mais aussi à les gérer, les stocker…

Pour suivre la structure d’une API Rest, on utilise les différentes méthodes HTTP (Get, Post, Put, Delete)

Flask nous permet de faire ça facilement, directement dans la définition de la route avec le paramètre methods :

# description: retourne la liste de tous les utilisateurs
@app.route("/users", methods=['GET'])
def get_users():
	return "not implemented"

# description: ajoute un utilisateur à la liste
@app.route("/users", methods=['POST'])
def create_user():
	return "not implemented"

Les valeurs possibles sont 'GET', 'POST', 'PUT' et 'DELETE'

Recevoir des données

Maintenant que l’on sait retourner toutes sortes de données dans les réponses de notre API, il faut que l’on apprenne à récupérer les données reçues dans les requêtes !

Il existe deux moyens principaux de passer une donnée dans une requête HTTP :

  • Dans les paramètres de l’URL
  • Dans le corps (body) de la requête

Dans les paramètres de la requête

Pour les paramètres de l’URL, c’est très simple, il suffit de lire request.args :

from flask import Flask, request #nouvel import (request)

# [...]

@app.route("/hello") # Exemple /hello?name=John
def hello_name():
    name = request.args.get("name")
    return "Hello, " + name

Pour tester : http://127.0.0.1:5000/hello?name=John

Dans le corps de la requête

Et il est également possible de récupérer les données envoyées en JSON dans le corps de la requête (body) pour les méthodes POST et PUT.

Par défaut, le contenu JSON sera transformé en tableau à clé, et accessible comme ceci :

@app.route("/users", methods=['POST'])
def create_user():
	json_data = request.get_json()
  # json_data = { "username": "...", "firstname": "...", "lastname": "..."}

Désérialiser le JSON

Tout comme le fait de sérialiser une instance de classe pour l’envoyer en réponse de l’API, récupérer le corps de la requête pour le stocker dans un objet est une bonne pratique !

Et grâce à la bibliothèque marshmallow, nous allons pouvoir stocker le contenu de la requête dans un objet (et valider les données) très simplement, sans rien installer de plus :

# description: ajoute un utilisateur à la liste
@app.route("/users", methods=['POST'])
def create_user():
    json_data = request.get_json()
    try:
        user = user_validator.load(json_data)
        if user:
            users.append(user)
            return "User created", 201
    except Exception as err:
        return err.__str__(), 400

Grâce à notre user_validator, une exception sera envoyée si l’objet reçu en entrée ne correspond pas au schéma attendu (et renverra une erreur 400) !

Créer une route dynamique

Pour définir une route qui prendra un ou plusieurs paramètres dans son URL, on pourra définir chaque paramètre sous la forme <type:nom>, comme ceci :

# description : Retourne l'utilisateur correspondant à l'id
@app.route("/users/<int:id>")
def get_user_by_id(id: int) :
	user = users[id]
	return user_validator.dump(user)

Essayez par vous même : http://127.0.0.1:5000/users/0

À noter que l’on aurait également pu passer une chaîne de caractère, comme un nom d’utilisateur, comme ceci : /users/<String:username>

En bonus : créer un DTO

Si vous n’êtes pas familier avec le concept de Data Transfer Object, vous pouvez lire notre article à ce sujet : https://code-garage.fr/blog/a-quoi-servent-les-data-transfer-objects-dto/

En résumé, un DTO est un validateur qui va nous servir uniquement à valider le contenu d’un objet qui transite par le réseau (ici, la réponse de l’API). C’est exactement le même concept de validation qu’avant, simplement on aura une classe dédiée à cette réponse :

class GetUsersResponse:
	def __init__(self, users):
		self.users = users
		self.count = len(users)

class GetUsersResponseDTO(Schema):
	users = fields.List(fields.Nested(UserSchema()))
	count = fields.Integer()
	
get_users_response_validator = GetUsersResponseDTO()

# description: retourne la liste de tous les utilisateurs
@app.route("/users")
def get_users() :
	response = GetUsersResponse(users);
	return get_users_response_validator.dump(response)

Cela vous permet en même temps de voir comment valider une liste d’objet avec Marshmallow, en utilisant fields.List(fields.Nested(UserSchema())) !

Le code complet

Retrouvez le code complet de ce tutoriel ci-dessous :

from flask import Flask, request
from marshmallow import Schema, fields

app = Flask(__name__)

### Classes

class User:
	username: str
	firstname: str
	lastname: str
	password: str
	def __init__(self, username: str, firstname: str, lastname: str):
		self.username = username
		self.firstname = firstname
		self.lastname = lastname
		self.password = "azejbnzdfunIJIN65" #généré aléatoirement

class UserSchema(Schema):
    username = fields.Str(required=True)
    firstname = fields.Str(required=True)
    lastname = fields.Str(required=True)

# On créé le validateur à partir de notre schéma
user_validator = UserSchema()

### Données

users = []
users.append(User("JohnDoe", "John", "Doe"))

### Exemples

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

@app.route("/json")
def hello_json():
    return {"str1": "Hello", "str2": "world"}
	# ou return ["hello", "world"]

@app.route("/hello") # Exemple /hello?name=John
def hello_name():
    name = request.args.get("name")
    return "Hello, " + name

### Routes

# description: retourne le premier utilisateur de la liste
@app.route("/user")
def get_user():
    first_user = users[0]
		# dump(...) permet de transformer une instance de classe en json
    response = user_validator.dump(first_user)
    return response

# description: ajoute un utilisateur à la liste
@app.route("/users", methods=['POST'])
def create_user():
    json_data = request.get_json()
    try:
        user = user_validator.load(json_data)
        if user:
            users.append(user)
            return "User created", 201
    except Exception as err:
        return err.__str__(), 400

# description : Retourne l'utilisateur correspondant à l'id
@app.route("/users/<int:id>")
def get_user_by_id(id: int) :
	user = users[id]
	return user_validator.dump(user)

class GetUsersResponse:
	def __init__(self, users):
		self.users = users
		self.count = len(users)

class GetUsersResponseDTO(Schema):
	users = fields.List(fields.Nested(UserSchema()))
	count = fields.Integer()
	
get_users_response_validator = GetUsersResponseDTO()

# description: retourne la liste de tous les utilisateurs
@app.route("/users")
def get_users() :
	response = GetUsersResponse(users);
	return get_users_response_validator.dump(response)

Pour lire la suite de ce tutoriel, vous devez posséder un compte gratuit