13 KiB
Démonstration d'API GraphQL
Installation
Ouvrir un terminal dans le dossier racine du projet :
poetry install # installer les dépendances nécessaires
Pensez également à marquer le dossier "src" comme "Source Root" (via clic droit "Mark Directory as ...") dans l'IDE PyCharm, pour éviter des problèmes d'importation.
Pour démarrer le serveur GraphQL, utilisez la commande :
# En étant à la racine du projet
cd src
uvicorn app.main:app --host 0.0.0.0 --port 8004
Ouvrez Appolo Studio à l'adresse suivante :git push -u origin main https://studio.apollographql.com/sandbox/explorer/
Bien rentrer dans l'outil l'URL complète :
À noter que vous pouvez aussi utiliser l'interface incluse GraphiQL en vous rendant à l'adresse : http://127.0.0.1:8004/graphql
Instructions
Vous allez devoir créer les fichiers manquants pour faire fonctionner l'application. Suivez attentivement les étapes.
Vue d'ensemble de l'architecture
Avant de commencer, voici un schéma illustrant comment les différentes parties que vous allez créer interagissent :
graph TD
A["Client Apollo/GraphiQL"] -- "Requete GraphQL" --> B{"Serveur Uvicorn/Strawberry"};
B -- "Traite la requete" --> C[Query / Mutation];
C -- "Appelle le bon resolver" --> D["Resolver ex: resolve_recipe_by_id"];
D -- "Demande les donnees" --> E["Service recipe_service"];
E -- "Recupere/Ecrit les donnees" --> F["Repository recipe_repository"];
F -- "Interagit" --> G["Base de Donnees 'simulee'"];
Étape 1 : les Modèles Métier (Pydantic)
Ce sont les représentations internes de nos données. Elles utilisent Pydantic pour la validation.
-
Créez le fichier
app/models/ingredient.pyet ajoutez-y le code suivant :# Fichier: app/models/ingredient.py from pydantic import BaseModel class Ingredient(BaseModel): name: str quantity: str -
Créez le fichier
app/models/recipe.pyet ajoutez-y ce code :# Fichier: app/models/recipe.py from pydantic import BaseModel from typing import List from .ingredient import Ingredient class Recipe(BaseModel): id: int title: str description: str ingredients: List[Ingredient]
Étape 2 : les types GraphQL (Strawberry)
Ce sont les "schémas" que notre API va exposer aux clients. Ils décrivent la forme des données que l'on peut demander.
-
Créez le dossier
app/graphql/types/. -
Créez le fichier
app/graphql/types/ingredient.py:# Fichier: app/graphql/types/ingredient.py import strawberry @strawberry.type class Ingredient: name: str quantity: str -
Créez le fichier
app/graphql/types/recipe.py:# Fichier: app/graphql/types/recipe.py import strawberry from typing import List from .ingredient import Ingredient @strawberry.type class Recipe: id: strawberry.ID title: str description: str ingredients: List[Ingredient]
Étape 3 : la logique métier (BLL / Services)
Le service fait le lien entre la couche API (les resolvers que nous verrons après) et la couche de données (le repository qui est déjà fourni).
- Créez le dossier
app/services/si nécessaire. - Créez le fichier
app/services/recipe_service.pyet ajoutez-y les fonctions suivantes :# Fichier: app/services/recipe_service.py from typing import List, Dict, Any from app.models.recipe import Recipe from app.repositories.recipe_repository import recipe_repository from app.core.exceptions import NotFoundBLLException, ValidationBLLException def get_all_recipes() -> List[Recipe]: """Récupère toutes les recettes.""" return recipe_repository.list() def get_recipe_by_id(recipe_id: int) -> Recipe: """ Récupère une recette par son ID. Lève une exception si la recette n'est pas trouvée. """ recipe = recipe_repository.find_by_id(recipe_id) if not recipe: raise NotFoundBLLException(resource_name="Recipe", resource_id=recipe_id) return recipe def add_new_recipe(title: str, description: str, ingredients: List[Dict[str, Any]]) -> Recipe: """ Ajoute une nouvelle recette après validation. """ if not title or not title.strip(): raise ValidationBLLException("Le titre de la recette ne peut pas être vide.") if not ingredients: raise ValidationBLLException("Une recette doit contenir au moins un ingrédient.") recipe_data = { "title": title, "description": description, "ingredients": ingredients } return recipe_repository.create(recipe_data)
Étape 4 : les Inputs GraphQL (pour les Mutations)
Quand on veut envoyer des données complexes au serveur (comme pour créer une nouvelle recette), on utilise des inputs.
-
Créez le dossier
app/graphql/inputs/. -
Créez le fichier
app/graphql/inputs/ingredient_input.py:# Fichier: app/graphql/inputs/ingredient_input.py import strawberry @strawberry.input class IngredientInput: name: str quantity: str -
Créez le fichier
app/graphql/inputs/recipe_input.py:# Fichier: app/graphql/inputs/recipe_input.py import strawberry from typing import List from .ingredient_input import IngredientInput @strawberry.input class AddRecipeInput: title: str description: str ingredients: List[IngredientInput]
Comprendre la différence : Model vs Type vs Input
Vous avez maintenant créé trois types de classes qui se ressemblent. Voici un schéma pour clarifier leurs rôles distincts :
graph TD
subgraph "Modele Metier 'Pydantic'"
A["class Recipe(BaseModel)<br><b>Role:</b> Validation et logique interne<br><b>Utilise par:</b> Services, Repository"]
end
subgraph "Schema GraphQL 'Strawberry'"
B["class Recipe(strawberry.type)<br><b>Role:</b> Definir ce qu'on peut lire 'Query'<br><b>Utilise pour:</b> Retourner des donnees"]
C["class AddRecipeInput(strawberry.input)<br><b>Role:</b> Definir ce qu'on peut ecrire 'Mutation'<br><b>Utilise pour:</b> Recevoir des donnees"]
end
E[Service] -- "Manipule et retourne un" --> A
A -- "Est converti en" --> B
B -- "Envoye au Client" --> D[Client GraphQL]
D -- "Envoie un" --> C
C -- "Est utilise par le" --> F[Resolver]
F -- "Pour appeler le" --> E
Étape 5 : les resolvers
Le resolver est la fonction qui "résout" une requête. C'est le cœur de notre API : il prend une demande GraphQL et appelle le service approprié pour obtenir les données.
- Créez le dossier
app/graphql/resolvers/. - Créez le fichier
app/graphql/resolvers/recipe_resolvers.pyet remplissez-le :# Fichier: app/graphql/resolvers/recipe_resolvers.py import strawberry from typing import List, Optional from app.graphql.types.recipe import Recipe from app.graphql.inputs.recipe_input import AddRecipeInput import app.services.recipe_service as recipe_service def resolve_recipes() -> List[Recipe]: """Resolver pour obtenir la liste de toutes les recettes.""" return recipe_service.get_all_recipes() def resolve_recipe_by_id(id: strawberry.ID) -> Optional[Recipe]: """Resolver pour obtenir une recette par son ID.""" return recipe_service.get_recipe_by_id(recipe_id=int(id)) def resolve_add_recipe(input: AddRecipeInput) -> Recipe: """Resolver pour ajouter une nouvelle recette.""" ingredients_data = [ {"name": ing.name, "quantity": ing.quantity} for ing in input.ingredients ] return recipe_service.add_new_recipe( title=input.title, description=input.description, ingredients=ingredients_data )
Étape 6 : assemblage final (Queries & Mutations)
Maintenant, nous allons déclarer les points d'entrée de notre API en les liant aux resolvers que nous venons de créer.
-
Créez le fichier
app/graphql/queries.py:# Fichier: app/graphql/queries.py import strawberry from typing import List, Optional from app.graphql.types.recipe import Recipe from app.graphql.resolvers.recipe_resolvers import resolve_recipes, resolve_recipe_by_id @strawberry.type class Query: recipes: List[Recipe] = strawberry.field( resolver=resolve_recipes, description="Récupère la liste de toutes les recettes de cuisine." ) recipe: Optional[Recipe] = strawberry.field( resolver=resolve_recipe_by_id, description="Récupère une recette par son ID." ) -
Créez le fichier
app/graphql/mutations.py:# Fichier: app/graphql/mutations.py import strawberry from app.graphql.types.recipe import Recipe from app.graphql.inputs.recipe_input import AddRecipeInput from app.graphql.resolvers.recipe_resolvers import resolve_add_recipe @strawberry.type class Mutation: addRecipe: Recipe = strawberry.field( resolver=resolve_add_recipe, description="Ajoute une nouvelle recette au livre de cuisine." )
Vous avez créé tous les fichiers nécessaires. L'application est maintenant complète.
7. Requêtes GraphQL via Appolo Studio
En s'assurant que votre serveur est bien démarré, vous pouvez éxécuter les requêtes GraphQL suivantes pour tester l'API :
Flux d'une requête (Query)
Voici ce qui se passe lorsque vous exécutez la requête recipe(id: "1") :
sequenceDiagram
actor Client
participant API as "GraphQL 'main.py'"
participant Query as "Query 'queries.py'"
participant Resolver as "Resolver 'recipe_resolvers.py'"
participant Service as "Service 'recipe_service.py'"
participant Repo as "Repository 'recipe_repository.py'"
Client->>+API: query { recipe(id: "1") }
API->>+Query: Trouve le champ "recipe"
Query->>+Resolver: Appelle "resolve_recipe_by_id(id='1')"
Resolver->>+Service: Appelle "get_recipe_by_id(recipe_id=1)"
Service->>+Repo: Appelle "find_by_id(1)"
Repo-->>-Service: Retourne "Recipe Model Pydantic"
Service-->>-Resolver: Retourne "Recipe Model Pydantic"
Resolver-->>-Query: Retourne le modele
Query-->>-API: Construit la reponse JSON
API-->>-Client: Reponse JSON
Flux d'une requête (Mutation)
Et voici ce qui se passe lors d'une mutation addRecipe :
sequenceDiagram
actor Client
participant API as "GraphQL 'main.py'"
participant Mutation as "Mutation 'mutations.py'"
participant Resolver as "Resolver 'recipe_resolvers.py'"
participant Service as "Service 'recipe_service.py'"
participant Repo as "Repository 'recipe_repository.py'"
Client->>+API: mutation { addRecipe(input: {...}) }
API->>+Mutation: Trouve le champ "addRecipe"
Mutation->>+Resolver: Appelle "resolve_add_recipe(input=InputObject)"
Resolver->>Resolver: Convertit "InputObject" en "dict"
Resolver->>+Service: Appelle "add_new_recipe(title, ...)"
Service->>Service: Valide les donnees "ex: titre non vide"
Service->>+Repo: Appelle "create(recipe_data)"
Repo-->>-Service: Retourne nouvelle "Recipe Model Pydantic"
Service-->>-Resolver: Retourne "Recipe Model Pydantic"
Resolver-->>-Mutation: Retourne le modele
Mutation-->>-API: Construit la reponse JSON
API-->>-Client: Reponse JSON
Exemples de requêtes
query GetAllRecipes {
recipes {
id
title
description
}
}
query GetRecipeById {
recipe(id: "1") {
id
title
description
ingredients {
name
quantity
}
}
}
query GetNonExistentRecipe {
recipe(id: "99") {
id
title
}
}
mutation AddNewRecipe {
addRecipe(
input: {
title: "Salade César"
description: "Une salade fraîche et croquante."
ingredients: [
{ name: "Laitue romaine", quantity: "1" }
{ name: "Poulet grillé", quantity: "150g" }
{ name: "Croûtons", quantity: "1 tasse" }
{ name: "Parmesan", quantity: "50g" }
]
}
) {
id
title
description
}
}
Vous pouvez si vous le souhaitez ajouter d'autres recettes via la mutation addRecipe et vérifier qu'elles apparaissent bien dans la liste retournée par la requête recipes.