Skip to Content
06 AdvancedGraphQL comme Port d'Entrée

GraphQL comme Port d’Entrée

Cette page présente l’utilisation de GraphQL comme port d’entrée dans une architecture hexagonale avec Micronaut.


Pourquoi GraphQL ?

GraphQL est une alternative à REST qui offre plus de flexibilité pour les clients.

API REST

// Récupérer un utilisateur GET /api/users/1 { "id": 1, "email": "john@example.com", "name": "John Doe", "createdAt": "2025-01-01T00:00:00Z" } // Récupérer ses commandes GET /api/users/1/orders [ { "id": 101, "total": 50.0, "status": "delivered" }, { "id": 102, "total": 30.0, "status": "pending" } ] // Récupérer les détails d'une commande GET /api/orders/101 { "id": 101, "total": 50.0, "items": [...] }

Problèmes :

  • Multiple requests : 3 appels pour avoir user + orders + items
  • Over-fetching : on reçoit tous les champs même si on n’a besoin que du name
  • Under-fetching : besoin de plusieurs appels pour avoir toutes les données

GraphQL = Flexibilité + Performance + Documentation automatique


Configuration Micronaut GraphQL

Ajouter la dépendance

<!-- pom.xml --> <dependency> <groupId>io.micronaut.graphql</groupId> <artifactId>micronaut-graphql</artifactId> </dependency>

Configurer GraphQL

# application.yml graphql: enabled: true path: /graphql graphiql: enabled: true path: /graphiql schema-generation: enabled: true

Endpoints disponibles :

  • POST /graphql : API GraphQL
  • GET /graphiql : Interface GraphiQL (développement)

Vérifier l’installation

# Démarrer l'application ./mvnw mn:run # Ouvrir GraphiQL http://localhost:8080/graphiql

GraphiQL est un IDE interactif pour tester vos queries GraphQL avec autocomplétion et documentation.


Schéma GraphQL

Le schéma GraphQL définit les types et opérations disponibles.

Définir le schéma

# src/main/resources/schema.graphqls type User { id: ID! email: String! name: String! createdAt: String! orders: [Order!]! } type Order { id: ID! userId: ID! total: Float! status: OrderStatus! items: [OrderItem!]! } type OrderItem { product: String! quantity: Int! price: Float! } enum OrderStatus { PENDING DELIVERED CANCELLED } type Query { user(id: ID!): User users: [User!]! order(id: ID!): Order } type Mutation { createUser(email: String!, name: String!): User! placeOrder(userId: ID!, items: [OrderItemInput!]!): Order! } input OrderItemInput { product: String! quantity: Int! price: Float! }

Placez le schéma GraphQL dans src/main/resources/schema.graphqls pour qu’il soit automatiquement chargé par Micronaut.


GraphQL Resolvers (Ports d’Entrée)

Les resolvers sont les adapters d’entrée qui font le pont entre GraphQL et les Use Cases.

        • UserQueryResolver.java
        • UserMutationResolver.java
        • OrderQueryResolver.java
        • UserDataFetcher.java

Query Resolver

package com.example.infrastructure.adapters.graphql; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import io.micronaut.graphql.annotation.GraphQLRepository; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; @Singleton @GraphQLRepository @RequiredArgsConstructor public class UserQueryResolver { private final GetUserUseCase getUserUseCase; private final GetAllUsersUseCase getAllUsersUseCase; public DataFetcher<User> user() { return env -> { Long id = Long.parseLong(env.getArgument("id")); return getUserUseCase.execute(id); }; } public DataFetcher<List<User>> users() { return env -> getAllUsersUseCase.execute(); } }

Attention : les resolvers sont des adapters d’entrée (infrastructure) qui appellent les Use Cases (application).


Architecture Hexagonale avec GraphQL

Voici l’architecture complète avec GraphQL comme port d’entrée.

┌─────────────────────────────────────────────────────────┐ │ Infrastructure │ ├─────────────────────────────────────────────────────────┤ │ │ │ GraphQL Resolvers (Adapters IN) │ │ ├─ UserQueryResolver │ │ ├─ UserMutationResolver │ │ └─ UserDataFetcher │ │ ↓ │ ├──────────────────────────────────────────────────────────┤ │ Application │ ├──────────────────────────────────────────────────────────┤ │ │ │ Use Cases (Ports IN) │ │ ├─ GetUserUseCase │ │ ├─ CreateUserUseCase │ │ └─ GetUserOrdersUseCase │ │ ↓ │ ├──────────────────────────────────────────────────────────┤ │ Domain │ ├──────────────────────────────────────────────────────────┤ │ │ │ Entities │ │ ├─ User │ │ └─ Order │ │ ↓ │ ├──────────────────────────────────────────────────────────┤ │ Application │ ├──────────────────────────────────────────────────────────┤ │ │ │ Repositories (Ports OUT) │ │ ├─ UserRepository │ │ └─ OrderRepository │ │ ↓ │ ├──────────────────────────────────────────────────────────┤ │ Infrastructure │ ├──────────────────────────────────────────────────────────┤ │ │ │ Repository Adapters (Adapters OUT) │ │ ├─ JpaUserRepository │ │ └─ JpaOrderRepository │ │ │ └──────────────────────────────────────────────────────────┘

GraphQL Resolver = Adapter d’entrée au même titre qu’un Controller REST.


Exemple Complet

Use Case

package com.example.application.usecases; import com.example.domain.model.User; import com.example.application.ports.out.UserRepository; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; @Singleton @RequiredArgsConstructor public class GetUserUseCase { private final UserRepository userRepository; public User execute(Long userId) { return userRepository.findById(userId) .orElseThrow(() -> new ResourceNotFoundException("User not found: " + userId)); } }

Problème N+1 et DataLoader

Le problème N+1 se produit quand une query déclenche N requêtes en base pour chaque élément.

Problème N+1

query { users { # 1 requête SQL id name orders { # N requêtes SQL (1 par user) id total } } }

Flux d’exécution :

-- Requête 1 : récupérer tous les users SELECT * FROM users; -- Requête 2 : récupérer les orders du user 1 SELECT * FROM orders WHERE user_id = 1; -- Requête 3 : récupérer les orders du user 2 SELECT * FROM orders WHERE user_id = 2; -- Requête N : récupérer les orders du user N SELECT * FROM orders WHERE user_id = N;

Résultat : 1 + N requêtes SQL (très inefficace)

DataLoader est crucial pour éviter le problème N+1 dans les queries GraphQL avec relations.


Gestion des Erreurs

GraphQL a un format d’erreur standardisé.

import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; import graphql.schema.DataFetcher; @Singleton @RequiredArgsConstructor public class UserQueryResolver { private final GetUserUseCase getUserUseCase; public DataFetcher<User> user() { return env -> { try { Long id = Long.parseLong(env.getArgument("id")); return getUserUseCase.execute(id); } catch (ResourceNotFoundException e) { throw GraphqlErrorBuilder.newError() .message(e.getMessage()) .path(env.getExecutionStepInfo().getPath()) .errorType(ErrorType.NOT_FOUND) .build(); } }; } }

Réponse d’erreur :

{ "data": { "user": null }, "errors": [ { "message": "User not found: 999", "path": ["user"], "errorType": "NOT_FOUND" } ] }

Tests

Tester un Resolver

@MicronautTest class UserQueryResolverTest { @Inject UserQueryResolver resolver; @MockBean(GetUserUseCase.class) GetUserUseCase getUserUseCase() { return mock(GetUserUseCase.class); } @Test void shouldReturnUser() throws Exception { // Given User user = new User(1L, "john@example.com", "John Doe", Instant.now()); when(getUserUseCase.execute(1L)).thenReturn(user); DataFetchingEnvironment env = mock(DataFetchingEnvironment.class); when(env.getArgument("id")).thenReturn("1"); // When User result = resolver.user().get(env); // Then assertThat(result.email()).isEqualTo("john@example.com"); verify(getUserUseCase).execute(1L); } }

Tester une Query GraphQL

@MicronautTest class GraphQLIntegrationTest { @Inject @Client("/graphql") HttpClient client; @Test void shouldGetUserById() { // Given String query = """ query { user(id: "1") { id email name } } """; Map<String, Object> request = Map.of("query", query); // When HttpResponse<Map> response = client.toBlocking().exchange( HttpRequest.POST("/", request), Map.class ); // Then assertThat(response.status()).isEqualTo(HttpStatus.OK); Map<String, Object> data = (Map<String, Object>) response.body().get("data"); Map<String, Object> user = (Map<String, Object>) data.get("user"); assertThat(user.get("email")).isEqualTo("john@example.com"); } }

Comparaison REST vs GraphQL

CritèreRESTGraphQL
EndpointsMultiples (/users, /orders)Un seul (/graphql)
Over-fetchingOui (tous les champs)Non (client choisit)
Under-fetchingOui (N requêtes)Non (une query)
Versioning/v1/users, /v2/usersÉvolution du schéma
CachingFacile (HTTP cache)Plus complexe
ComplexitéSimplePlus complexe (N+1, DataLoader)
DocumentationSwagger/OpenAPIIntrospection automatique
Cas d’usageCRUD simple, public APIsApps complexes, mobile

Recommandation :

  • REST : APIs publiques, CRUD simple, besoin de caching HTTP
  • GraphQL : Apps frontend complexes, mobile, micro-frontends

Bonnes Pratiques

Règles d’or pour GraphQL :

  1. Schéma first : définir le schéma GraphQL avant le code
  2. DataLoader : toujours utiliser pour les relations (éviter N+1)
  3. Pagination : implémenter la pagination pour les listes (first, after)
  4. Error handling : utiliser le format GraphQL standard
  5. Validation : valider les inputs dans les Use Cases (pas dans les resolvers)
  6. Depth limiting : limiter la profondeur des queries (éviter les queries malveillantes)
  7. Query complexity : calculer la complexité des queries et rejeter les plus coûteuses
  8. Monitoring : logger les queries lentes avec Micrometer

Pagination avec GraphQL

type Query { users(first: Int, after: String): UserConnection! } type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! } type UserEdge { node: User! cursor: String! } type PageInfo { hasNextPage: Boolean! endCursor: String }

Resolver avec pagination :

public DataFetcher<UserConnection> users() { return env -> { Integer first = env.getArgument("first"); String after = env.getArgument("after"); Page<User> page = getAllUsersUseCase.execute(first, after); return new UserConnection( page.users().stream() .map(user -> new UserEdge(user, encodeCursor(user.id()))) .toList(), new PageInfo(page.hasNextPage(), encodeCursor(page.lastUserId())) ); }; }

Références


Prochaine étape : Stratégies de Migration

Last updated on