Aller au contenu
BackJavaquarkusGraphQL

La Taverne GraphQL : découvrir GraphQL avec Quarkus

Dans la taverne The Falling Whale, chaque aventurier vient chercher des informations différentes. Avec GraphQL et Quarkus, découvrez comment construire une API flexible où chaque client commande exactement les données dont il a besoin.

nos aventuriers prêt à partir à l'aventure

la traditionnelle musique de mes articles sur GraphQL

Dans les auberges du royaume, les aventuriers ont tous des besoins différents.

Le guerrier veut simplement connaître le prix d'une chambre et la liste des quêtes disponibles. Le mage réclame des détails complets sur les récompenses, les objets magiques et les créatures rencontrées. Le voleur, lui, ne veut récupérer qu'une poignée d'informations discrètes avant de disparaître dans les ruelles.

Dans une API REST classique, chaque client reçoit souvent beaucoup plus d'informations que nécessaire. Certaines routes deviennent très spécialisées, d'autres enchaînent plusieurs appels successifs, et les échanges réseau finissent par ressembler à une longue tournée de tavernes dans tout le royaume : beaucoup de chemin parcouru, beaucoup d'énergie dépensée, pour récupérer au final quelques informations utiles noyées dans un flot de données inutiles.

C'est précisément là que GraphQL entre en scène. Avec GraphQL, le client demande exactement les données qu'il souhaite récupérer, ni plus, ni moins, comme un aventurier qui tends sa liste de commandes précise au tavernier plutôt que de lui demander de tout apporter depuis les cuisines.
Et avec Quarkus et SmallRye GraphQL, il devient possible de construire rapidement une telle taverne directement à partir du code Java.

Installer SmallRye GraphQL dans Quarkus

Avant d'accueillir les aventuriers, encore faut-il construire la taverne.

La vieille bâtisse existe déjà, ses murs sont solides et son équipe rodée, mais il lui manque un comptoir digne de ce nom, capable d'encaisser les demandes les plus précises sans s'épuiser à tout servir d'un coup. Dans l'écosystème Quarkus, ce comptoir s'appelle SmallRye GraphQL. Cette extension implémente la spécification MicroProfile GraphQL et génère automatiquement un schéma GraphQL à partir du code Java, sans qu'il soit nécessaire d'écrire manuellement le moindre fichier .graphqls.
Le Java devient la source de vérité, le plan de la taverne depuis lequel tout le reste est construit.

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-graphql</artifactId>
</dependency>

la dépendance

Une fois l'application démarrée avec quarkus dev, Quarkus expose automatiquement deux adresses utiles :

  • /graphql/schema.graphql pour consulter le plan de salle généré
  • /q/graphql-ui pour tester les commandes directement depuis le navigateur, comme un tavernier qui vérifie son menu avant d'ouvrir
GraphQL UI

La taverne ouvre officiellement ses portes.

Ouvrir la taverne : créer un endpoint GraphQL

Dans une API REST classique, chaque type de demande aurait nécessité sa propre route, son propre couloir de service, son propre garçon attitré. Avec GraphQL, la taverne fonctionne autrement : tout passe par un comptoir unique, derrière lequel deux types d'opérations cohabitent.
Les Query permettent de consulter le registre sans rien y modifier, comme feuilleter le grand livre des chambres.
Les Mutation permettent d'y inscrire de nouveaux noms, de changer l'état du monde.

Notre taverne va donc ouvrir trois guichets : la liste des aventuriers présents, la consultation d'un aventurier précis par son identifiant, et l'enregistrement d'un nouvel arrivant.

@GraphQLApi
@ApplicationScoped
public class TaverneResource {

    @Inject
    TaverneService taverneService;

    @Query("aventuriers")
    @Description("Liste tous les aventuriers présents dans la taverne.")
    public @NonNull List<@NonNull AventurierResponse> aventuriers() {
        return taverneService.getAllAventuriers().stream()
                .map(AventurierResponse::fromDomain)
                .toList();
    }

    @Query("aventurier")
    @Description("Recherche un aventurier par son identifiant unique.")
    public AventurierResponse aventurier(@Name("id") @NonNull Long id) {
        return AventurierResponse.fromDomain(taverneService.getAventurier(id));
    }

    @Mutation("ajouterAventurier")
    @Description("Ajoute un nouvel aventurier dans la taverne.")
    public @NonNull AventurierResponse ajouterAventurier(@Name("input") @NonNull AventurierInput input) {
        var aventurier = taverneService.ajouterAventurier(
                input.nom(),
                input.classe(),
                input.niveau()
        );
        return AventurierResponse.fromDomain(aventurier);
    }
}

notre controller

L'annotation @GraphQLApi est la clé qui ouvre la taverne. Sans elle, Quarkus voit simplement un bean CDI ordinaire, un bâtiment fermé sans enseigne.
Avec elle, SmallRye GraphQL comprend qu'il doit analyser les méthodes annotées et construire automatiquement le schéma GraphQL.

Chaque méthode Java devient alors une capacité offerte par la taverne, une spécialité inscrite au menu.

Les aventuriers deviennent des types GraphQL

Derrière le comptoir, le tavernier tient un registre soigné. Chaque aventurier y est décrit avec précision :

  • son nom,
  • sa classe,
  • son niveau d'expérience,
  • les quêtes qu'il a acceptées ou terminées.

Ce registre, dans notre taverne GraphQL, prend la forme de records Java que SmallRye GraphQL transforme automatiquement en types GraphQL sans qu'il soit nécessaire d'en écrire la description à la main.

@Description("Données publiques d'un aventurier")
public record AventurierResponse(
    @NonNull @Description("Identifiant unique") Long id,
    @NonNull @Description("Nom de l'aventurier") String nom,
    @NonNull @Description("Classe (ex: Guerrier, Mage)") String classe,
    @NonNull @Description("Niveau d'expérience") Integer niveau,
    @NonNull @Description("Liste des quêtes en cours ou terminées") List<@NonNull QueteResponse> quetes
) {
    public static AventurierResponse fromDomain(Aventurier a) {
        if (a == null) return null;
        return new AventurierResponse(
            a.id, a.nom, a.classe, a.niveau,
            a.quetes != null ? a.quetes.stream().map(QueteResponse::fromDomain).toList() : List.of()
        );
    }
}

Les quêtes disposent de leur propre entrée dans le registre :

@Description("Détails d'une quête")
public record QueteResponse(
    @NonNull @Description("Identifiant de la quête") Long id,
    @NonNull @Description("Intitulé") String titre,
    @Description("Niveau de difficulté") String difficulte,
    @Description("Montant de la prime") Integer recompenseOr
) {
    public static QueteResponse fromDomain(Quete q) {
        if (q == null) return null;
        return new QueteResponse(q.id, q.titre, q.difficulte, q.recompenseOr);
    }
}

Les records Java conviennent particulièrement bien à cet usage : immuables, explicites, lisibles, chaque composant devient naturellement un champ GraphQL.

Mais ce qui rend ce registre vraiment utile, c'est l'annotation @Description. Grâce à elle, la documentation Java est directement injectée dans le schéma GraphQL, et le registre se tient tout seul, sans qu'aucun tavernier n'ait à le recopier à la main.

Les inputs : accueillir un nouvel aventurier

Lorsqu'un aventurier pousse la porte pour la première fois et demande à s'inscrire dans le registre, le tavernier ne lui tend pas le grand livre ouvert à n'importe quelle page.

Il lui présente un formulaire précis, taillé pour l'occasion : nom, classe choisie, niveau initial. Rien de plus, rien de moins.

C'est exactement ce que représente un objet input dans GraphQL.

@Name("AventurierInput")
@Description("Données pour la création d'un aventurier")
public record AventurierInput(
    @NonNull @Description("Nom de l'aventurier") String nom,
    @NonNull @Description("Classe choisie") String classe,
    @NonNull @Description("Niveau initial (doit être > 0)") Integer niveau
) {}

notre input aventurier

Ce formulaire d'accueil ne représente pas un aventurier complet. Il représente uniquement ce que le tavernier a besoin de savoir pour ouvrir une nouvelle page dans le registre.
L'identifiant, lui, reste de la responsabilité de la taverne : aucun aventurier ne choisit lui-même son numéro de chambre.
Cette séparation entre ce que le client fournit et ce que la taverne expose protège le registre de toute tentative de manipulation, et rend le contrat d'API plus clair, plus solide, plus facile à faire évoluer sans tout réécrire.

Le service métier : la vraie vie de la taverne

Un comptoir bien tenu ne suffit pas à faire tourner une taverne. Derrière la salle, dans les cuisines et les réserves, une organisation entière s'active : les règles sur les réservations, la gestion des stocks, les décisions sur les quêtes acceptées ou refusées. Cette logique ne doit pas s'étaler sur le comptoir.

Elle doit rester à sa place, invisible depuis la salle, mais parfaitement structurée.

@ApplicationScoped
public class TaverneService {

    private final List<Aventurier> aventuriers = new ArrayList<>();

    public List<Aventurier> getAllAventuriers() {
        if (aventuriers.isEmpty()) {
            initData();
        }
        return List.copyOf(aventuriers);
    }

    private void initData() {
        Aventurier guerrier = new Aventurier();
        guerrier.id = 1L;
        guerrier.nom = "Baldric";
        guerrier.classe = "Guerrier";
        guerrier.niveau = 12;

        Quete quete = new Quete();
        quete.id = 100L;
        quete.titre = "Nettoyer les caves infestées";
        quete.difficulte = "Moyenne";
        quete.recompenseOr = 250;

        guerrier.quetes = List.of(quete);
        aventuriers.add(guerrier);
    }
}

le service

À l'ouverture, la taverne initialise automatiquement son premier client : Baldric le Guerrier, déjà inscrit dans le registre avec sa quête en cours, prêt à apparaître dans les premières requêtes GraphQL.

Mais ce qui compte ici, c'est surtout que le endpoint GraphQL ne sait rien des cuisines. Il tend les commandes au service, attend les réponses, et les transmet au client.

Validations, persistance, transactions, règles métier complexes : tout cela reste derrière la porte des cuisines. GraphQL ne remplace pas l'architecture de la taverne. Il en remplace uniquement le comptoir.

Interroger la taverne : les premières Query

La taverne est ouverte, le registre est tenu, Baldric attend sa première chope. Il est temps d'envoyer les premières commandes depuis l'interface GraphQL, accessible sur http://localhost:8080/q/graphql-ui.

Un aventurier pressé entre et veut simplement savoir qui est présent et à quel niveau. Il n'a pas besoin de connaître les quêtes, les récompenses ou les détails des créatures rencontrées. Il pose sa question, et la taverne lui répond exactement ce qu'il a demandé :

query {
  aventuriers {
    nom
    niveau
  }
}
{
  "data": {
    "aventuriers": [
      {
        "nom": "Baldric",
        "niveau": 12
      }
    ]
  }
}

Un autre aventurier entre à sa suite, plus curieux, et veut connaître les quêtes disponibles avec leurs récompenses. Il compose sa propre commande, sans que personne n'ait eu besoin de créer une nouvelle route, de modifier le backend ou de versionner quoi que ce soit :

query {
  aventuriers {
    nom
    classe
    quetes {
      titre
      recompenseOr
    }
  }
}

C'est là toute la force de GraphQL : chaque client compose sa propre réponse, comme chaque aventurier choisit son repas depuis le même menu, sans imposer aux autres ce qu'ils doivent commander.

Les mutations : inscrire un nouvel aventurier

Eldric le Mage pousse la porte de la taverne. Niveau 18, cape usée par les routes, il cherche un endroit pour s'inscrire avant de partir en quête. Le tavernier sort le formulaire d'accueil, Eldric remplit les cases, et son nom rejoint le registre.

mutation {
  ajouterAventurier(
    input: {
      nom: "Eldric"
      classe: "Mage"
      niveau: 18
    }
  ) {
    id
    nom
    classe
  }
}
{
  "data": {
    "ajouterAventurier": {
      "id": 2,
      "nom": "Eldric",
      "classe": "Mage"
    }
  }
}

Eldric ne demande que l'essentiel : son identifiant, son nom, sa classe. Il n'a pas besoin de voir le détail de ses quêtes, qui n'existent pas encore. Dans une API REST classique, le serveur aurait renvoyé l'objet complet sans se poser la question. Ici, c'est Eldric qui décide ce qu'il récupère en sortant du comptoir.

Comprendre le schéma généré automatiquement

Le registre de la taverne ne s'est pas écrit tout seul par magie. SmallRye GraphQL l'a construit en lisant attentivement le code Java, en relevant chaque annotation, chaque contrainte de nullabilité, chaque description. Le résultat est un schéma GraphQL complet, consultable sur /graphql/schema.graphql, sans qu'une seule ligne de SDL ait été rédigée à la main.

"Données publiques d'un aventurier"
type AventurierResponse {
  "Classe (ex: Guerrier, Mage)"
  classe: String!
  "Identifiant unique"
  id: BigInteger!
  "Niveau d'expérience"
  niveau: Int!
  "Nom de l'aventurier"
  nom: String!
  "Liste des quêtes en cours ou terminées"
  quetes: [QueteResponse!]!
}

"Mutation root"
type Mutation {
  "Ajoute un nouvel aventurier dans la taverne."
  ajouterAventurier(input: AventurierInput!): AventurierResponse!
}

"Query root"
type Query {
  "Recherche un aventurier par son identifiant unique."
  aventurier(id: BigInteger!): AventurierResponse
  "Liste tous les aventuriers présents dans la taverne."
  aventuriers: [AventurierResponse!]!
}

"Détails d'une quête"
type QueteResponse {
  "Niveau de difficulté"
  difficulte: String
  "Identifiant de la quête"
  id: BigInteger!
  "Montant de la prime"
  recompenseOr: Int
  "Intitulé"
  titre: String!
}

"Données pour la création d'un aventurier"
input AventurierInput {
  "Classe choisie"
  classe: String!
  "Niveau initial (doit être > 0)"
  niveau: Int!
  "Nom de l'aventurier"
  nom: String!
}

notre schéma auto généré

Le ! qui suit chaque type indique qu'un champ ne peut jamais être nul, comme une entrée du registre qu'on ne peut pas laisser vide.

La ligne quetes: [QueteResponse!]! signifie que ni la liste elle-même ni aucune des quêtes qu'elle contient ne peuvent être nulles, une garantie qui vient directement du Java : List<@NonNull QueteResponse>.

La null-safety ne reste pas enfermée dans le code. Elle traverse la porte et devient une partie du contrat visible par tous les clients de la taverne.

Pourquoi GraphQL fonctionne bien avec Quarkus

Quarkus cherche à tenir une taverne légère : démarrage rapide, faible consommation de ressources, expérience de développement fluide, taillée pour les environnements cloud où chaque seconde et chaque mégaoctet comptent. GraphQL cherche à tenir un comptoir précis : moins d'allers-retours inutiles, des clients qui ne reçoivent que ce qu'ils ont commandé, un contrat d'API qui peut évoluer sans tout casser.

Les deux philosophies se renforcent naturellement. Un panneau d'affichage pour les quêtes, un registre des chambres et une carte interactive du royaume peuvent tous interroger la même API GraphQL, chacun en ne demandant que ce dont il a besoin, sans que la taverne ait à tenir un comptoir différent pour chaque type de client.

REST contre GraphQL : deux visions de la taverne

Toutes les tavernes du royaume ne fonctionnent pas de la même façon, et c'est tant mieux. Certaines servent un menu fixe, rapide, efficace, sans discussion : on commande le plat du jour, on reçoit le plat du jour. REST fonctionne ainsi, et pour beaucoup de besoins, c'est exactement ce qu'il faut : des API simples, des endpoints techniques, des traitements CRUD classiques où la ressource prime sur la flexibilité.

D'autres tavernes, plus fréquentées, accueillent des clients aux profils très différents dont les besoins varient du tout au tout. Servir à tous le même plateau serait un gaspillage énorme, et multiplier les menus spéciaux pour chaque type de visiteur deviendrait ingérable. C'est là que GraphQL prend tout son sens : quand plusieurs clients consomment la même API avec des attentes radicalement différentes, quand les objets sont fortement liés, quand les écrans évoluent vite et que les allers-retours réseau commencent à peser sur l'expérience.

L'objectif n'est pas de choisir un camp. C'est de savoir quel type de taverne convient au royaume qu'on cherche à servir.

Conclusion

Avec quelques annotations et très peu de configuration, Quarkus et SmallRye GraphQL permettent de construire rapidement une taverne moderne : un comptoir unique, un registre auto-documenté, un contrat fortement typé, et des clients qui ne repartent qu'avec ce qu'ils ont commandé.

Nous avons vu comment exposer des Query et des Mutation, comment le schéma GraphQL se génère automatiquement depuis le Java, comment les records deviennent des types GraphQL, comment la null-safety traverse la porte pour devenir partie intégrante du contrat d'API, et comment séparer proprement le comptoir des cuisines.

Et surtout, nous avons vu la philosophie centrale de GraphQL : laisser le client demander exactement ce dont il a besoin. Dans une taverne pleine d'aventuriers aux attentes différentes, c'est probablement la meilleure manière de satisfaire tout le royaume.

Dernier