Le développement d’applications web modernes repose aujourd'hui sur des technologies qui offrent flexibilité, rapidité et efficacité. L’un des stacks les plus prometteurs dans ce domaine est le FARM, une combinaison puissante de FastAPI, React et MongoDB, qui permet de créer des applications web rapides et évolutives. Mais pourquoi choisir cette combinaison et comment chaque composant de ce stack contribue-t-il à la réussite d’un projet ?

La première pierre angulaire du FARM stack est FastAPI, un framework web pour Python qui se distingue par sa rapidité et sa facilité d’utilisation. Grâce à FastAPI, le développement d'API RESTful devient un jeu d'enfant, permettant aux développeurs de se concentrer sur la logique métier plutôt que sur la gestion des requêtes HTTP ou des sérialisations complexes. Ce framework permet de créer des endpoints API performants, tout en garantissant une syntaxe claire et lisible grâce à son utilisation de la programmation asynchrone. L’un des principaux avantages de FastAPI réside dans sa capacité à générer automatiquement une documentation interactive de l’API, ce qui facilite grandement la maintenance et l’utilisation de l’application à long terme.

En ce qui concerne le frontend, React est le choix incontournable pour des interfaces utilisateur dynamiques et réactives. Cette bibliothèque JavaScript développée par Facebook offre une architecture basée sur des composants, permettant une gestion modulaire du code et une mise à jour en temps réel de l'interface sans recharger la page. React est apprécié pour sa rapidité de rendu grâce à son DOM virtuel, ce qui permet aux applications de réagir instantanément aux interactions des utilisateurs sans compromis sur les performances. Son écosystème, riche et en constante évolution, offre une multitude de bibliothèques et d’outils permettant de personnaliser et d'étendre les fonctionnalités d'une application.

Enfin, le stack FARM intègre MongoDB, une base de données NoSQL particulièrement adaptée aux applications modernes, où les données sont souvent non structurées ou semi-structurées. Contrairement aux bases de données relationnelles traditionnelles, MongoDB utilise un modèle de données flexible, permettant de stocker des documents JSON-like dans des collections. Cela permet une grande agilité dans le développement, particulièrement lorsqu’il s’agit de gérer des volumes de données importants ou des données en évolution rapide. MongoDB est également conçu pour être hautement scalable, ce qui en fait une solution idéale pour les applications qui doivent gérer de grandes quantités d’utilisateurs ou de données.

Le choix du stack FARM repose sur une logique simple mais puissante : combiner des technologies qui, individuellement, sont déjà extrêmement performantes, mais qui, ensemble, permettent de créer des applications web robustes, modulaires et performantes. FastAPI assure une communication rapide et fiable entre le frontend et le backend, React offre une expérience utilisateur fluide et interactive, tandis que MongoDB garantit une gestion efficace des données à grande échelle. Cette combinaison permet de répondre aux exigences des applications modernes qui nécessitent à la fois de la rapidité, de la flexibilité et de la scalabilité.

Il est important de souligner que l’utilisation de ce stack n’est pas seulement une question de performance ou de modularité. Le FARM stack est aussi une réponse aux défis actuels du développement web. Le monde des applications web évolue à une vitesse vertigineuse, avec de nouvelles fonctionnalités et technologies apparaissant constamment. Le FARM stack permet de s'adapter rapidement à ces changements, en offrant un cadre flexible qui peut évoluer en fonction des besoins spécifiques d’un projet ou des évolutions technologiques.

Enfin, bien qu'il soit tentant de se concentrer uniquement sur la rapidité et l’efficacité des technologies individuelles, le véritable atout du FARM stack réside dans son approche cohérente et intégrée. Chaque composant de ce stack est conçu pour travailler ensemble de manière optimale, minimisant ainsi les problèmes d'intégration ou de compatibilité qui peuvent survenir lorsque l'on utilise des technologies disparates. De plus, la documentation et la communauté autour de chacune de ces technologies sont extrêmement solides, ce qui facilite l’apprentissage et la résolution de problèmes.

Au-delà de la simple maîtrise des technologies, il est essentiel de comprendre que le développement d'une application avec le FARM stack nécessite également une approche rigoureuse de la conception et de la gestion des données. Par exemple, bien que MongoDB offre une grande flexibilité, cela implique également une gestion attentive de la structure des données et de l'indexation pour garantir des performances optimales à mesure que l’application évolue. De plus, même si FastAPI simplifie la création des API, il est crucial de bien comprendre la logique asynchrone et la gestion des erreurs pour éviter les problèmes de performance à grande échelle. Enfin, React, bien qu’exceptionnel dans la gestion de l’interface utilisateur, requiert une bonne maîtrise de ses concepts de base et de ses mécanismes d'état pour créer une expérience utilisateur fluide et réactive.

En résumé, le stack FARM représente une approche moderne et cohérente pour le développement d’applications web. Ce n’est pas seulement une question de choisir les bonnes technologies, mais de comprendre comment elles interagissent pour offrir une solution performante, évolutive et maintenable. Maîtriser ce stack exige un apprentissage approfondi, mais les récompenses en termes de rapidité de développement et de satisfaction des utilisateurs en valent largement la peine.

Comment utiliser Pydantic pour la validation et la personnalisation des modèles de données ?

Pydantic est une bibliothèque Python de validation et de parsing des données, qui repose sur des types de données standard comme les chaînes de caractères, les entiers, les dictionnaires ou les ensembles. Cette approche est très intuitive et permet de démarrer rapidement, surtout lorsqu’il s’agit de gérer des entrées simples. Cependant, Pydantic offre bien plus que la validation de types primitifs. En effet, il fournit une large gamme de types et de contraintes pour valider des données complexes. Ces fonctionnalités permettent d’augmenter la souplesse et la précision des modèles.

Les types stricts de Pydantic, tels que StrictBool, StrictInt, StrictStr et d’autres, imposent que les valeurs validées appartiennent exactement à ces types, sans aucun type de coercition. Par exemple, un StrictInt doit être de type entier, et ne peut accepter des valeurs comme "1" (chaîne de caractères) ou 1.0 (flottant), ce qui renforce la rigueur de la validation.

Les types contraints, comme condate() ou conlist(), ajoutent des règles supplémentaires aux types standards. Ainsi, condate() permet de valider des dates avec des contraintes de comparaison comme "supérieur à", "inférieur à", etc. conlist(), quant à lui, permet de valider des listes en imposant des contraintes sur leur longueur ou en s'assurant que les éléments de la liste sont uniques. Ces types sont des outils puissants dans des cas d’utilisation où la précision des données est cruciale.

Pydantic va au-delà de la simple validation de types primitifs et propose de nombreux validateurs prêts à l’emploi pour des cas d’usage fréquents. Par exemple, le validateur EmailStr permet de valider des adresses email. Toutefois, il est important de noter que ce validateur, étant externe à la bibliothèque principale de Pydantic, doit être installé séparément via la commande pip install pydantic[email]. Sur le site officiel de Pydantic, on trouve une liste exhaustive des types de validation disponibles, allant des longueurs minimales et maximales pour les listes aux formats de codes de couleur CSS. Ce niveau de personnalisation permet à Pydantic de couvrir un large éventail de besoins de validation.

Lorsque vous travaillez avec des modèles dans Pydantic, il est aussi possible d’ajouter des métadonnées à vos champs en utilisant la classe Field. Cela permet de personnaliser les modèles, de définir des valeurs par défaut explicites, et même d’ajouter des alias pour les champs. Un alias est particulièrement utile lorsqu’on interagit avec des systèmes externes ou des API qui utilisent des noms de champs différents. Par exemple, si un système tiers renvoie des données dans un format différent, comme un champ user_id au lieu de id, vous pouvez utiliser un alias pour adapter ce champ au modèle attendu par votre application.

Prenons l’exemple d’un modèle de données pour un utilisateur avec des champs comme id, username, email, etc. En utilisant la classe Field, vous pouvez spécifier un alias pour chaque champ qui a un nom différent dans les données externes. Si l'API renvoie un user_id au lieu d’un id, vous pouvez facilement créer un alias pour ce champ dans votre modèle Pydantic :

python
class UserModelFields(BaseModel):
id: int = Field(alias="user_id") username: str = Field(alias="name") email: str = Field() account: Literal["personal", "business"] | None = Field(default=None, alias="account_type") nickname: str | None = Field(default=None, alias="nick")

Cela permet à Pydantic de mapper correctement les données externes tout en maintenant la structure interne de votre modèle de manière cohérente.

Un autre aspect intéressant des champs Pydantic est la possibilité d’imposer des contraintes sur les valeurs numériques. Prenons l'exemple d'un tournoi d'échecs modélisé avec Pydantic :

python
from datetime import datetime
from uuid import uuid4 from pydantic import BaseModel, Field class ChessTournament(BaseModel): id: int = Field(strict=True) dt: datetime = Field(default_factory=datetime.now) name: str = Field(min_length=10, max_length=30) num_players: int = Field(ge=4, le=16, multiple_of=2) code: str = Field(default_factory=uuid4)

Ici, plusieurs validations sont appliquées sur les champs :

  • dt utilise le paramètre default_factory pour attribuer automatiquement la date et l’heure actuelles à chaque instance du modèle.

  • name est contraint à une longueur minimale et maximale, garantissant que le nom du tournoi reste dans une plage définie.

  • num_players est restreint entre 4 et 16, et doit être un multiple de 2 pour garantir que chaque joueur puisse participer à des rondes d’échecs de manière équilibrée.

  • code utilise également un default_factory pour générer automatiquement un code unique à l’aide de la bibliothèque uuid.

  • id utilise le paramètre strict=True pour imposer que l’identifiant soit un entier pur, et pas une chaîne ou un flottant.

Ces fonctionnalités permettent de valider des données de manière très précise et concise, en évitant des vérifications manuelles complexes et répétitives.

La sérialisation des données constitue également une partie importante du travail avec Pydantic. Lorsque vous devez convertir un modèle Pydantic en un dictionnaire Python ou une chaîne JSON, la méthode model_dump() est utilisée. Cela vous permet de facilement extraire les données validées de votre modèle, et de les utiliser dans des processus comme la sauvegarde dans une base de données ou l'envoi à une API.

Pydantic n’est donc pas seulement un outil de validation de données, mais une solution complète pour structurer et gérer des modèles de données complexes. En utilisant des types personnalisés, des alias et des contraintes sur les champs, vous pouvez facilement adapter votre modèle aux besoins spécifiques de votre application tout en maintenant une rigueur dans la validation des données.

La documentation officielle de Pydantic, particulièrement la section dédiée aux champs, est une ressource essentielle à explorer pour découvrir l’ensemble des options de validation disponibles et s’assurer d’utiliser la bibliothèque de manière optimale dans vos projets. La puissance de Pydantic réside dans sa capacité à rendre les modèles de données simples, clairs et rigoureusement validés.

Comment gérer l'authentification et les utilisateurs dans FastAPI avec Beanie et JWT

La classe d'authentification encapsule la logique de gestion des utilisateurs, y compris l'enregistrement, la connexion et la vérification via des tokens Bearer, tout en utilisant le mécanisme de sécurité JWT pour l'encodage et le décodage des informations utilisateur. Ce mécanisme est identique à celui présenté précédemment dans le chapitre 6, sur l'authentification et l'autorisation, et l'implémentation de la logique d'authentification et d'autorisation repose sur le fichier authentication.py. Ce fichier contient la gestion des tokens JWT, le hachage des mots de passe avec bcrypt et l'injection de dépendances, comme expliqué dans le chapitre 7, lors de la construction d'une API backend avec FastAPI.

Dans le fichier authentication.py, la classe AuthHandler gère l'ensemble des fonctionnalités liées à la sécurité, comme suit : la gestion des mots de passe avec la classe CryptContext, l'encodage et le décodage des tokens JWT à l'aide de la bibliothèque jwt, ainsi que la vérification des signatures. Le secret utilisé pour l'encodage est un simple FARMSTACKsecretString, mais il est fortement recommandé d'utiliser une clé secrète plus robuste et unique en production.

Voici le code de base pour cette classe :

python
import datetime import jwt from fastapi import HTTPException, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from passlib.context import CryptContext class AuthHandler: security = HTTPBearer() pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") secret = "FARMSTACKsecretString" def get_password_hash(self, password): return self.pwd_context.hash(password) def verify_password(self, plain_password, hashed_password): return self.pwd_context.verify(plain_password, hashed_password) def encode_token(self, user_id, username): payload = { "exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=30), "iat": datetime.datetime.now(datetime.timezone.utc), "sub": {"user_id": user_id, "username": username}, } return jwt.encode(payload, self.secret, algorithm="HS256") def decode_token(self, token): try: payload = jwt.decode(token, self.secret, algorithms=["HS256"]) return payload["sub"] except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="Signature has expired") except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token")
def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)):
return self.decode_token(auth.credentials)

L'implémentation d'un fichier user.py pour gérer les routes API des utilisateurs se place dans le dossier /routers. Ce fichier expose trois points d'entrée : pour l'enregistrement de nouveaux utilisateurs, la connexion, et la vérification de l'utilisateur avec un token Bearer. Le routeur pour l'enregistrement et la connexion des utilisateurs est conçu de manière similaire à l'exemple avec Motor, mais les spécificités liées à Beanie sont également prises en compte.

Voici le code pour le routeur d'utilisateur avec FastAPI :

python
from fastapi import APIRouter, Body, Depends, HTTPException from fastapi.responses import JSONResponse from authentication import AuthHandler from models import CurrentUser, LoginUser, RegisterUser, User auth_handler = AuthHandler() router = APIRouter() @router.post("/register", response_description="Register user", response_model=CurrentUser)
async def register(newUser: RegisterUser = Body(...), response_model=User):
newUser.password = auth_handler.get_password_hash(newUser.password) query = {
"$or": [{"username": newUser.username}, {"email": newUser.email}]} existing_user = await User.find_one(query) if existing_user is not None: raise HTTPException(status_code=409, detail=f"{newUser.username} or {newUser.email} already exists") user = await User(**newUser.model_dump()).save() return user @router.post("/login", response_description="Login user and return token")
async def login(loginUser: LoginUser = Body(...)) -> str:
user =
await User.find_one(User.username == loginUser.username) if user and auth_handler.verify_password(loginUser.password, user.password): token = auth_handler.encode_token(str(user.id), user.username) response = JSONResponse(content={"token": token, "username": user.username}) return response else: raise HTTPException(status_code=401, detail="Invalid username or password") @router.get("/me", response_description="Logged in user data", response_model=CurrentUser) async def me(user_data=Depends(auth_handler.auth_wrapper)): currentUser = await User.get(user_data["user_id"]) return currentUser

Ce code de base permet à l'utilisateur de s'enregistrer, de se connecter et de récupérer ses informations utilisateur en utilisant un token JWT pour la vérification. Lors de l'enregistrement d'un nouvel utilisateur, un contrôle de la disponibilité du nom d'utilisateur et de l'email est effectué pour éviter les doublons dans la base de données. La création d'un utilisateur se fait de manière asynchrone en utilisant la méthode save() de Beanie.

Il est important de souligner que Beanie simplifie l'interaction avec MongoDB en permettant de définir des modèles Pydantic pour la validation des données, tout en offrant une interface intuitive pour effectuer des requêtes sur la base de données. L'utilisation de find_one() et d'autres méthodes asynchrones facilite l'intégration avec FastAPI, tout en garantissant des performances élevées et une architecture claire.

En ce qui concerne la vérification de l'utilisateur, un token JWT est décodé à chaque requête avec la méthode auth_wrapper, qui permet d'extraire les informations de l'utilisateur à partir du token pour les utiliser dans les différentes routes protégées.

Pour enrichir ce chapitre, il serait utile de discuter de l'importance de sécuriser les tokens JWT en production, notamment en utilisant HTTPS et en s'assurant que les tokens sont bien stockés dans des cookies sécurisés ou dans des en-têtes appropriés. Il est également recommandé de discuter des stratégies d'actualisation des tokens pour garantir une expérience utilisateur fluide sans compromettre la sécurité.

Enfin, la gestion des erreurs et la mise en place de messages clairs en cas d'échec d'authentification ou d'enregistrement sont essentielles pour améliorer l'expérience utilisateur et faciliter le débogage.