Créer une API REST avec Python et Flask
Créer une API REST avec Python et Flask, c’est un peu comme construire une maison bien pensée : au début, tout semble simple, puis très vite on comprend que la solidité se joue dans les fondations. Une API n’est pas seulement un ensemble d’URL qui retournent du JSON. C’est un contrat entre votre application et le monde extérieur, une manière claire, prévisible et maintenable d’exposer des données, des actions et des règles métier. Avec Flask, on obtient une base légère, élégante et très agréable à prendre en main. Avec Python, on garde une syntaxe lisible, une grande richesse d’outils et une productivité très appréciable. Ensemble, ils forment un duo qui convient aussi bien à un projet personnel qu’à une application professionnelle.
Dans cet article, on va construire une API REST propre, progressive et réaliste avec Flask. On va partir des bases, puis aller vers une structure plus sérieuse, avec l’organisation du code, les routes, la validation des données, la gestion des erreurs, les bonnes pratiques de conception, la connexion à une base de données, l’utilisation des méthodes HTTP, les réponses JSON, les codes de statut, la sécurité minimale et quelques touches utiles pour rendre le tout vraiment exploitable. Le but n’est pas seulement de “faire fonctionner” une API, mais de comprendre comment la concevoir proprement dès le départ, afin d’éviter les mauvaises surprises quand le projet grandit.
Comprendre ce qu’est une API REST
Avant de plonger dans le code, il vaut la peine de rappeler ce qu’on appelle une API REST. REST signifie “Representational State Transfer”. Derrière ce terme parfois impressionnant se cache une idée simple : utiliser le protocole HTTP de manière cohérente pour manipuler des ressources. Une ressource peut être un utilisateur, un article, une tâche, un produit, une commande, ou n’importe quel objet de votre application. Chaque ressource possède une identité, généralement représentée par une URL, et on agit dessus avec les verbes HTTP standards : GET, POST, PUT, PATCH, DELETE.
Par exemple, une API REST pour gérer des tâches peut exposer :
GET /taskspour lister les tâchesGET /tasks/1pour récupérer une tâche précisePOST /taskspour créer une tâchePUT /tasks/1pour remplacer une tâchePATCH /tasks/1pour modifier partiellement une tâcheDELETE /tasks/1pour supprimer une tâche
La beauté de REST, c’est la cohérence. Quand on découvre une API bien construite, on peut presque deviner son fonctionnement en regardant ses routes. C’est rassurant pour le développeur qui l’utilise, et c’est précieux pour l’équipe qui la maintient.
Pourquoi choisir Flask pour une API REST
Flask est un micro-framework Python. Le mot “micro” ne veut pas dire qu’il est limité, mais plutôt qu’il est volontairement minimaliste. Flask vous donne l’essentiel : gestion des routes, requêtes, réponses, contexte applicatif, templates si besoin, et une grande liberté pour organiser le reste à votre façon. Pour une API REST, cette liberté est un avantage énorme.
Avec Flask, vous n’êtes pas enfermé dans une architecture rigide. Vous pouvez démarrer petit, comprendre ce que vous faites, puis faire évoluer votre projet au rythme des besoins réels. C’est particulièrement intéressant pour :
un prototype rapide,
une API d’administration,
un backend pour application mobile,
un service interne,
un projet de portfolio,
ou une API de production bien organisée.
Flask se marie aussi très bien avec des extensions comme Flask-SQLAlchemy, Flask-Migrate, Flask-Marshmallow, Flask-RESTful ou Flask-JWT-Extended. Mais on peut déjà faire une API REST très propre avec Flask seul, en ajoutant seulement ce qui est réellement utile. C’est exactement ce que nous allons faire ici.
Préparer l’environnement du projet
Commençons par créer un environnement isolé. C’est une habitude à garder systématiquement, parce qu’elle évite bien des conflits entre versions de bibliothèques.
mkdir flask-rest-api
cd flask-rest-api
python -m venv venv
Activez ensuite l’environnement virtuel :
Sur Windows :
venv\Scripts\activate
Sur macOS ou Linux :
source venv/bin/activate
Installez Flask et quelques dépendances utiles :
pip install flask flask-sqlalchemy flask-migrate marshmallow flask-marshmallow
Pour ce tutoriel, on utilisera aussi flask-cors si l’on souhaite autoriser un front-end séparé, ainsi que python-dotenv pour gérer les variables d’environnement plus proprement.
pip install flask-cors python-dotenv
La structure de départ peut ressembler à ceci :
flask-rest-api/
│
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ ├── schemas.py
│ └── config.py
│
├── migrations/
├── .env
├── run.py
└── requirements.txt
Cette organisation n’est pas la seule possible, mais elle a l’avantage d’être claire. On sépare la configuration, les modèles, les schémas de validation et les routes. C’est une base saine dès le début.
Créer une première application Flask
Pour comprendre l’idée générale, commençons par une version très simple. Dans run.py, on peut créer une app minimale :
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({
"message": "Bienvenue sur mon API REST avec Flask"
})
if __name__ == "__main__":
app.run(debug=True)
Lancez l’application :
python run.py
En allant sur http://127.0.0.1:5000/, vous devriez voir une réponse JSON contenant un message de bienvenue. Ce premier test paraît anodin, mais il confirme l’essentiel : Flask sait déjà répondre proprement en JSON, et votre environnement fonctionne.
Pour une vraie API, on va rapidement faire mieux que tout mettre dans un seul fichier. Le but est de construire quelque chose de lisible et extensible.
Structurer proprement l’application
Dans un projet sérieux, il est préférable d’adopter une structure modulaire. L’idée générale est de séparer la création de l’application, les extensions, les routes, les modèles et la configuration.
Dans app/__init__.py :
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_marshmallow import Marshmallow
from flask_cors import CORS
from .config import Config
db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
CORS(app)
from .routes import api_bp
app.register_blueprint(api_bp, url_prefix="/api")
return app
Cette approche, souvent appelée “application factory”, est très pratique. Elle évite d’instancier l’application trop tôt, facilite les tests et permet de gérer plusieurs environnements sans douleur.
Dans app/config.py :
import os
class Config:
SECRET_KEY = os.getenv("SECRET_KEY", "une-cle-secrete-par-defaut")
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
Dans run.py :
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
Vous avez maintenant une base plus propre. L’application sera plus simple à faire évoluer, notamment si vous ajoutez plusieurs modules métier.
Définir un premier modèle de données
Prenons un cas concret : une API de gestion de tâches. Chaque tâche aura un titre, une description, un état et une date de création. Dans app/models.py :
from datetime import datetime
from . import db
class Task(db.Model):
__tablename__ = "tasks"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
description = db.Column(db.Text, nullable=True)
completed = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
def __repr__(self):
return f"<Task {self.title}>"
Ce modèle est simple, mais il contient déjà plusieurs notions essentielles : une clé primaire, des champs obligatoires, des types explicites, une valeur par défaut et une date de création. Dans les projets réels, cette logique peut se complexifier, mais le principe reste identique.
Pour créer la base de données, on peut utiliser Flask-Migrate. Initialisez d’abord les migrations :
flask db init
flask db migrate -m "Création du modèle Task"
flask db upgrade
Si votre variable d’environnement n’est pas encore configurée pour Flask, vous pouvez définir l’application explicitement. Par exemple :
export FLASK_APP=run.py
Sur Windows PowerShell :
$env:FLASK_APP="run.py"
Avec SQLite, le démarrage est très facile. Plus tard, vous pourrez passer à PostgreSQL ou MySQL sans changer toute l’architecture.
Valider et sérialiser les données avec Marshmallow
Une API REST ne doit pas seulement enregistrer des données, elle doit aussi vérifier qu’elles sont valides avant de les accepter. C’est là que Marshmallow devient précieux. Il permet de définir des schémas de sérialisation et de désérialisation, c’est-à-dire de transformer vos objets Python en JSON et inversement, tout en validant les entrées.
Dans app/schemas.py :
from . import ma
from .models import Task
class TaskSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Task
load_instance = True
include_relationships = True
On peut créer deux instances du schéma : une pour un seul objet, et une pour une liste.
task_schema = TaskSchema()
tasks_schema = TaskSchema(many=True)
Cette étape est importante parce qu’elle évite d’écrire partout du code de conversion manuel. Elle améliore aussi la cohérence des réponses API.
Construire les routes de l’API
C’est maintenant que l’API prend vie. Dans app/routes.py :
from flask import Blueprint, request, jsonify
from . import db
from .models import Task
from .schemas import TaskSchema, tasks_schema, task_schema
api_bp = Blueprint("api", __name__)
@api_bp.route("/tasks", methods=["GET"])
def get_tasks():
tasks = Task.query.all()
return jsonify(tasks_schema.dump(tasks)), 200
Cette route récupère toutes les tâches et les renvoie en JSON. La réponse est claire, structurée, et utilise un code HTTP adapté.
Ajoutons maintenant la création d’une tâche :
@api_bp.route("/tasks", methods=["POST"])
def create_task():
data = request.get_json()
if not data:
return jsonify({"error": "Aucune donnée reçue"}), 400
title = data.get("title")
description = data.get("description")
if not title:
return jsonify({"error": "Le champ 'title' est obligatoire"}), 400
new_task = Task(
title=title,
description=description
)
db.session.add(new_task)
db.session.commit()
return jsonify({
"message": "Tâche créée avec succès",
"task": task_schema.dump(new_task)
}), 201
Cette route est déjà fonctionnelle, mais elle mérite mieux encore. En particulier, on verra plus loin comment rendre la validation plus élégante avec Marshmallow ou d’autres règles métier.
Ajoutons la récupération d’une tâche précise :
@api_bp.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
task = Task.query.get_or_404(task_id)
return jsonify(task_schema.dump(task)), 200
La méthode get_or_404 simplifie le code et renvoie automatiquement une erreur 404 si l’enregistrement n’existe pas. C’est pratique et lisible.
Pour modifier une tâche :
@api_bp.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
task = Task.query.get_or_404(task_id)
data = request.get_json()
if not data:
return jsonify({"error": "Aucune donnée reçue"}), 400
title = data.get("title")
description = data.get("description")
completed = data.get("completed")
if title is not None:
task.title = title
if description is not None:
task.description = description
if completed is not None:
task.completed = completed
db.session.commit()
return jsonify({
"message": "Tâche mise à jour avec succès",
"task": task_schema.dump(task)
}), 200
Enfin, pour supprimer une tâche :
@api_bp.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
task = Task.query.get_or_404(task_id)
db.session.delete(task)
db.session.commit()
return jsonify({
"message": "Tâche supprimée avec succès"
}), 200
Avec cela, vous disposez déjà d’une vraie API CRUD. Elle permet de créer, lire, modifier et supprimer des tâches. C’est une base très solide pour comprendre le développement d’une API REST avec Flask.
Comprendre les méthodes HTTP avec finesse
Beaucoup de débutants utilisent les méthodes HTTP sans vraiment en sentir la logique. Pourtant, les choisir correctement améliore la clarté de l’API.
GET sert à récupérer des données sans modifier l’état du serveur. Il doit être sûr et idempotent. Cela signifie qu’un même appel répété n’est pas censé provoquer d’effet secondaire.
POST sert à créer une ressource ou à déclencher une action non idempotente. Dans notre cas, créer une tâche avec POST /tasks est parfaitement adapté.
PUT est souvent utilisé pour remplacer complètement une ressource. En pratique, beaucoup d’API l’utilisent aussi pour faire une mise à jour complète ou quasi complète.
PATCH sert plutôt à la mise à jour partielle. Si vous ne souhaitez modifier qu’un seul champ, PATCH est souvent plus sémantique.
DELETE permet de supprimer une ressource.
Ces détails peuvent sembler théoriques, mais ils comptent. Une API bien pensée rend le travail plus simple à ceux qui l’utilisent. Et quand on développe côté front-end, mobile ou intégration externe, cette clarté fait une énorme différence.
Améliorer la validation des données
Le code précédent fonctionne, mais il contient une validation manuelle assez basique. C’est acceptable pour un petit projet, mais dans une vraie application, il vaut mieux centraliser et structurer la validation.
Avec Marshmallow, vous pouvez définir des règles plus claires. Dans app/schemas.py :
from marshmallow import fields, validate
from . import ma
from .models import Task
class TaskSchema(ma.SQLAlchemyAutoSchema):
title = fields.String(required=True, validate=validate.Length(min=3, max=120))
description = fields.String(required=False, allow_none=True)
completed = fields.Boolean(required=False)
class Meta:
model = Task
load_instance = True
On peut ensuite utiliser ce schéma dans la route de création :
from marshmallow import ValidationError
@api_bp.route("/tasks", methods=["POST"])
def create_task():
try:
data = request.get_json()
task = task_schema.load(data)
db.session.add(task)
db.session.commit()
return jsonify({
"message": "Tâche créée avec succès",
"task": task_schema.dump(task)
}), 201
except ValidationError as err:
return jsonify({
"errors": err.messages
}), 400
Cette approche est beaucoup plus propre. Elle déplace la logique de validation dans le schéma, ce qui évite la répétition et améliore la lisibilité. En d’autres termes, votre API devient plus facile à maintenir, parce que les règles sont au bon endroit.
Gérer les erreurs de façon cohérente
Une API sérieuse doit répondre de manière cohérente quand une erreur survient. Il ne faut pas laisser le hasard décider du format d’erreur. Un client API doit pouvoir s’appuyer sur des réponses prévisibles.
Vous pouvez créer des gestionnaires d’erreurs pour harmoniser les réponses :
from flask import jsonify
def register_error_handlers(app):
@app.errorhandler(404)
def not_found(error):
return jsonify({
"error": "Ressource non trouvée"
}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({
"error": "Erreur interne du serveur"
}), 500
Puis appeler cette fonction dans create_app() :
from .errors import register_error_handlers
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
CORS(app)
from .routes import api_bp
app.register_blueprint(api_bp, url_prefix="/api")
register_error_handlers(app)
return app
Cette cohérence est très appréciée côté consommateur de l’API. Elle simplifie aussi le débogage. Quand le format des erreurs est stable, on gagne beaucoup de temps dans les phases d’intégration.
Adopter de bonnes réponses HTTP
Le code de statut HTTP est plus qu’un détail technique. C’est une partie de la conversation entre votre API et son client.
Voici quelques codes utiles à garder en tête :
200 OK: la requête a réussi201 Created: une ressource a été créée204 No Content: la requête a réussi, mais il n’y a pas de contenu à renvoyer400 Bad Request: la requête est invalide401 Unauthorized: authentification manquante ou invalide403 Forbidden: l’accès est refusé404 Not Found: la ressource n’existe pas409 Conflict: conflit logique, par exemple une ressource déjà existante422 Unprocessable Entity: données reçues, mais invalides sémantiquement500 Internal Server Error: erreur côté serveur
Utiliser les bons statuts améliore la qualité globale de votre API. Cela permet aux front-ends, aux outils d’intégration et aux développeurs d’agir correctement selon le cas.
Ajouter un peu de structure au projet
Quand une API grandit, une séparation logique des responsabilités devient indispensable. On peut aller plus loin en divisant les routes par domaine métier. Par exemple :
app/
├── __init__.py
├── config.py
├── models.py
├── schemas.py
├── errors.py
├── tasks/
│ ├── __init__.py
│ └── routes.py
└── users/
├── __init__.py
└── routes.py
Chaque module peut contenir ses propres routes et éventuellement ses propres services. Cette approche permet de limiter l’encombrement. On évite aussi le fameux fichier monstre qui devient illisible après quelques semaines.
Dans app/tasks/routes.py :
from flask import Blueprint
tasks_bp = Blueprint("tasks", __name__)
@tasks_bp.route("/", methods=["GET"])
def list_tasks():
return {"message": "Liste des tâches"}
Puis enregistrez le blueprint dans l’application :
from .tasks.routes import tasks_bp
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
CORS(app)
from .routes import api_bp
app.register_blueprint(api_bp, url_prefix="/api")
app.register_blueprint(tasks_bp, url_prefix="/api/tasks")
register_error_handlers(app)
return app
Cette organisation devient vite très utile si vous ajoutez des utilisateurs, de l’authentification, des rôles, des permissions ou des ressources multiples.
Ajouter la pagination
Une API qui renvoie des centaines ou des milliers d’éléments d’un coup devient rapidement lourde. La pagination est donc une excellente habitude.
Voici une version simple :
@api_bp.route("/tasks", methods=["GET"])
def get_tasks():
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 10, type=int)
pagination = Task.query.paginate(page=page, per_page=per_page, error_out=False)
tasks = pagination.items
return jsonify({
"items": tasks_schema.dump(tasks),
"page": pagination.page,
"per_page": pagination.per_page,
"total": pagination.total,
"pages": pagination.pages
}), 200
La pagination améliore les performances perçues et la lisibilité des réponses. Elle permet aussi au client de contrôler ce qu’il charge réellement. Dans des contextes d’interface web ou mobile, cette petite amélioration change beaucoup de choses.
Ajouter le tri et la recherche
Quand une ressource contient beaucoup d’éléments, il devient utile d’ajouter des filtres. Par exemple, on peut trier les tâches par date ou chercher par mot-clé.
Exemple de recherche simple :
@api_bp.route("/tasks/search", methods=["GET"])
def search_tasks():
query = request.args.get("q", "", type=str)
tasks = Task.query.filter(Task.title.ilike(f"%{query}%")).all()
return jsonify(tasks_schema.dump(tasks)), 200
Ou encore le tri :
@api_bp.route("/tasks", methods=["GET"])
def get_tasks():
sort = request.args.get("sort", "created_at")
order = request.args.get("order", "desc")
query = Task.query
if sort == "title":
if order == "asc":
query = query.order_by(Task.title.asc())
else:
query = query.order_by(Task.title.desc())
else:
if order == "asc":
query = query.order_by(Task.created_at.asc())
else:
query = query.order_by(Task.created_at.desc())
tasks = query.all()
return jsonify(tasks_schema.dump(tasks)), 200
Ces petites fonctionnalités rendent votre API plus agréable à utiliser. Elles montrent aussi que vous avez pensé au confort de consommation des données, pas seulement à leur stockage.
Gérer les variables d’environnement proprement
Une bonne API ne doit jamais stocker ses secrets directement dans le code. Les variables d’environnement sont un réflexe sain. On les utilise pour la clé secrète, l’URL de la base de données, les paramètres de production, et tout ce qui ne doit pas être exposé dans le dépôt.
Créer un fichier .env :
SECRET_KEY=ma-cle-secrete
DATABASE_URL=sqlite:///app.db
FLASK_ENV=development
Puis charger ces valeurs :
from dotenv import load_dotenv
load_dotenv()
Au démarrage de l’application, vous pouvez importer load_dotenv() dans run.py ou dans app/__init__.py. Cela permet de garder le code plus flexible et plus sécurisé.
Comprendre l’authentification dans une API REST
Très souvent, une API REST n’existe pas seule. Elle doit protéger certaines routes. La question de l’authentification arrive donc rapidement. Une solution courante consiste à utiliser des tokens JWT.
Avec Flask-JWT-Extended, on peut mettre en place une authentification simple et efficace. Installez d’abord la dépendance :
pip install flask-jwt-extended
Dans la configuration :
from datetime import timedelta
class Config:
SECRET_KEY = os.getenv("SECRET_KEY", "une-cle-secrete-par-defaut")
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "jwt-secret-key")
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
Dans app/__init__.py :
from flask_jwt_extended import JWTManager
jwt = JWTManager()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
jwt.init_app(app)
CORS(app)
from .routes import api_bp
app.register_blueprint(api_bp, url_prefix="/api")
register_error_handlers(app)
return app
Une route de connexion peut ressembler à ceci :
from flask_jwt_extended import create_access_token
from datetime import timedelta
@api_bp.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data.get("username")
password = data.get("password")
if username == "admin" and password == "admin123":
access_token = create_access_token(identity=username)
return jsonify({
"access_token": access_token
}), 200
return jsonify({
"error": "Identifiants invalides"
}), 401
Puis, pour protéger une route :
from flask_jwt_extended import jwt_required
@api_bp.route("/protected", methods=["GET"])
@jwt_required()
def protected():
return jsonify({
"message": "Vous êtes authentifié"
}), 200
Bien sûr, dans une vraie application, les identifiants doivent être vérifiés contre une base de données, et les mots de passe doivent être hachés correctement. Mais cette base montre le principe général.
Hacher les mots de passe
Il ne faut jamais stocker un mot de passe en clair. C’est une règle absolue. Utilisez un hachage sécurisé, comme ceux fournis par Werkzeug ou bcrypt.
Avec Werkzeug :
from werkzeug.security import generate_password_hash, check_password_hash
Pour enregistrer un mot de passe :
hashed_password = generate_password_hash(password)
Pour le vérifier :
check_password_hash(user.password, password)
Cette étape n’est pas une option. Elle fait partie du minimum vital en matière de sécurité.
Penser à la sécurité dès le départ
Même pour une petite API, quelques principes sont indispensables.
D’abord, validez toujours les entrées utilisateur. Ne faites jamais confiance aux données reçues. Ensuite, renvoyez des erreurs claires mais sans divulguer d’informations sensibles. Évitez de montrer des traces techniques trop détaillées en production. Enfin, protégez les routes sensibles et limitez les privilèges.
On peut aussi limiter les requêtes abusives, utiliser HTTPS en production, configurer correctement les CORS, journaliser les événements importants et surveiller les erreurs. Une API REST bien conçue n’est pas seulement fonctionnelle, elle est aussi résistante et prévisible.
Gérer les CORS
Si votre front-end est séparé de votre API, vous allez probablement rencontrer les CORS. Ce sujet peut sembler agaçant au début, mais il est assez simple à comprendre : le navigateur bloque certaines requêtes cross-origin pour des raisons de sécurité. Quand vous développez un front-end React, Vue, Angular ou mobile, il faut généralement autoriser explicitement les domaines concernés.
Avec Flask-CORS :
from flask_cors import CORS
CORS(app)
Pour une configuration plus précise :
CORS(app, resources={r"/api/*": {"origins": "*"}})
En production, il vaut mieux être plus restrictif qu’en développement. Autoriser tout le monde n’est pas toujours souhaitable.
Tester l’API
Une API ne devrait pas être livrée sans tests. Même quelques tests de base apportent énormément de confiance. Flask s’intègre très bien avec pytest.
Installez pytest :
pip install pytest
Créez un fichier tests/test_tasks.py :
import json
def test_get_tasks(client):
response = client.get("/api/tasks")
assert response.status_code == 200
Un test plus complet pour la création :
def test_create_task(client):
payload = {
"title": "Apprendre Flask",
"description": "Créer une API REST propre"
}
response = client.post(
"/api/tasks",
data=json.dumps(payload),
content_type="application/json"
)
assert response.status_code == 201
assert "task" in response.get_json()
Pour que cela fonctionne proprement, il faut configurer un client de test et idéalement une base temporaire. Ce sujet pourrait faire l’objet d’un article à part entière, mais l’idée générale est simple : plus vos tests couvrent les cas essentiels, plus vous pouvez faire évoluer votre code sans crainte.
Documenter l’API
Une API sans documentation est souvent une API difficile à adopter. La documentation n’est pas un luxe. C’est un outil de travail. Vous pouvez rédiger un README, décrire les routes, les paramètres, les réponses, les codes d’erreur, et fournir quelques exemples d’appels.
Exemple de documentation simple dans le README :
## GET /api/tasks
Retourne la liste des tâches.
Réponse 200 :
```json
[
{
"id": 1,
"title": "Apprendre Flask",
"description": "Créer une API REST propre",
"completed": false
}
]
Vous pouvez aller plus loin avec Swagger/OpenAPI, via `flask-smorest`, `flask-restx` ou d’autres solutions similaires. Dans un contexte professionnel, cette couche documentaire fait gagner beaucoup de temps à toute l’équipe.
## Passer de SQLite à PostgreSQL
SQLite est excellent pour démarrer. Il est simple, rapide à mettre en place et suffisant pour beaucoup de cas de test. Mais dès que l’application prend de l’ampleur, PostgreSQL devient souvent un meilleur choix. Il est plus robuste, plus adapté à la concurrence et mieux équipé pour les usages sérieux.
Pour passer à PostgreSQL, il suffit généralement de changer l’URI de connexion :
```python
SQLALCHEMY_DATABASE_URI = "postgresql://user:password@localhost:5432/ma_base"
Et si vous utilisez les variables d’environnement :
DATABASE_URL=postgresql://user:password@localhost:5432/ma_base
Le fait d’avoir bien structuré votre application dès le départ rend cette transition beaucoup plus fluide.
Exemple complet d’une petite API de tâches
Voici un exemple condensé, cohérent et utile pour mieux voir l’ensemble.
app/__init__.py :
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_marshmallow import Marshmallow
from flask_cors import CORS
from .config import Config
db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
CORS(app)
from .routes import api_bp
app.register_blueprint(api_bp, url_prefix="/api")
return app
app/models.py :
from datetime import datetime
from . import db
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
description = db.Column(db.Text, nullable=True)
completed = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
app/schemas.py :
from . import ma
from .models import Task
class TaskSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Task
load_instance = True
task_schema = TaskSchema()
tasks_schema = TaskSchema(many=True)
app/routes.py :
from flask import Blueprint, request, jsonify
from marshmallow import ValidationError
from . import db
from .models import Task
from .schemas import task_schema, tasks_schema
api_bp = Blueprint("api", __name__)
@api_bp.route("/tasks", methods=["GET"])
def get_tasks():
tasks = Task.query.order_by(Task.created_at.desc()).all()
return jsonify(tasks_schema.dump(tasks)), 200
@api_bp.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
task = Task.query.get_or_404(task_id)
return jsonify(task_schema.dump(task)), 200
@api_bp.route("/tasks", methods=["POST"])
def create_task():
try:
data = request.get_json()
if not data:
return jsonify({"error": "Aucune donnée reçue"}), 400
task = task_schema.load(data)
db.session.add(task)
db.session.commit()
return jsonify({
"message": "Tâche créée avec succès",
"task": task_schema.dump(task)
}), 201
except ValidationError as err:
return jsonify({"errors": err.messages}), 400
@api_bp.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
task = Task.query.get_or_404(task_id)
data = request.get_json()
if not data:
return jsonify({"error": "Aucune donnée reçue"}), 400
task.title = data.get("title", task.title)
task.description = data.get("description", task.description)
task.completed = data.get("completed", task.completed)
db.session.commit()
return jsonify({
"message": "Tâche mise à jour avec succès",
"task": task_schema.dump(task)
}), 200
@api_bp.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
task = Task.query.get_or_404(task_id)
db.session.delete(task)
db.session.commit()
return jsonify({
"message": "Tâche supprimée avec succès"
}), 200
run.py :
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
Ce petit ensemble constitue une vraie base d’API REST. Ce n’est pas encore une architecture d’entreprise complète, mais c’est exactement le type de structure qui vous permet d’apprendre sérieusement et d’évoluer proprement.
Quelques bonnes pratiques à garder en tête
Quand on crée une API REST, il est facile de se concentrer uniquement sur le code qui “marche”. Pourtant, ce sont souvent les détails de conception qui font la différence entre un projet fragile et un projet durable. Voici quelques habitudes qui valent vraiment l’effort :
Évitez les routes ambiguës. Une route doit dire clairement ce qu’elle fait. Préférez des noms cohérents et centrés sur les ressources. Utilisez des noms au pluriel pour les collections, comme /tasks ou /users. Gardez les codes HTTP logiques. Ne mélangez pas données et logique métier dans les routes si vous pouvez l’éviter. Extrayez les règles réutilisables dans des services ou des helpers. Ne laissez pas la validation se disperser partout. Pensez à la maintenance future, pas seulement au sprint actuel.
Il y a aussi une vérité très humaine dans le développement backend : un projet semble toujours simple au début. Puis les besoins réels arrivent. On ajoute un filtre, puis un utilisateur, puis un statut, puis une permission, puis une intégration externe, et tout à coup les décisions initiales comptent énormément. C’est pour cela qu’une API pensée avec soin dès les premières lignes vous rendra service longtemps.
Quand Flask est le bon choix, et quand il faut réfléchir
Flask est excellent pour une API REST légère à moyenne, pour un service bien cadré, pour une application sur laquelle vous voulez garder la main, ou pour un projet que vous souhaitez faire évoluer par étapes. Il est aussi parfait si vous aimez comprendre ce qui se passe sous le capot.
En revanche, si vous savez déjà que votre projet va nécessiter beaucoup de conventions strictes, un cadre très complet dès le départ, ou une architecture orientée “batteries included”, il peut être utile d’évaluer d’autres solutions selon le contexte. Cela dit, Flask reste un choix extrêmement respectable, solide et durable. Son écosystème et sa simplicité en font un excellent compagnon pour la plupart des APIs courantes.
Conclusion
Créer une API REST avec Python et Flask, ce n’est pas seulement apprendre à écrire quelques routes. C’est apprendre à penser en ressources, à structurer le code avec intelligence, à valider les données, à renvoyer des réponses cohérentes, à respecter le langage HTTP et à préparer le terrain pour un projet qui peut grandir sans devenir chaotique. Flask offre une approche à la fois simple et puissante. Python apporte sa lisibilité et sa richesse. Ensemble, ils permettent de construire des APIs élégantes, maintenables et agréables à faire évoluer.
La meilleure manière d’apprendre reste souvent de construire un petit projet concret, puis de l’améliorer peu à peu. Commencez par une ressource simple, comme des tâches ou des notes. Ajoutez ensuite la validation. Puis la pagination. Puis l’authentification. Puis les tests. Puis la documentation. Chaque étape vous rapproche d’une vraie maîtrise, parce qu’elle vous oblige à comprendre non seulement le “comment”, mais aussi le “pourquoi”.
Et c’est souvent là que se produit le déclic : on ne regarde plus une API comme une suite de requêtes et de réponses, mais comme un système vivant, clair, structuré, au service d’une expérience plus fluide pour les autres développeurs, les applications clientes et, au fond, pour vous-même