GraphQL Avec Spring Boot
GraphQL Avec Spring Boot
1 Introduction à GraphQL 1
1.1 Historique et motivations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Architecture générale de GraphQL . . . . . . . . . . . . . . . . . . . . . . 2
1.3 GraphQL vs REST : comparaison détaillée . . . . . . . . . . . . . . . . . . 3
1.4 Les trois opérations fondamentales . . . . . . . . . . . . . . . . . . . . . . 4
1.4.1 Query (lecture) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4.2 Mutation (écriture) . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4.3 Subscription (temps réel) . . . . . . . . . . . . . . . . . . . . . . . . 5
4 Queries et résolveurs 24
4.1 Le flux d’exécution d’une query . . . . . . . . . . . . . . . . . . . . . . . . 24
4.2 Chaîne de résolution des champs . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3 Repositories Spring Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.4 Couche service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.5 Contrôleurs GraphQL (résolveurs) . . . . . . . . . . . . . . . . . . . . . . . 28
i
TABLE DES MATIÈRES
5 Mutations 32
5.1 Principe des mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.2 Implémentation des mutations . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.3 Objets Input (DTOs) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.4 Validation des entrées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.5 Service PostService complet . . . . . . . . . . . . . . . . . . . . . . . . . . 35
5.6 Exemples de mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
8 Pagination et filtrage 49
8.1 Stratégies de pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.2 Pagination Offset (classique) . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.3 Pagination Cursor (Relay-style) . . . . . . . . . . . . . . . . . . . . . . . . 50
8.4 Filtrage et tri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
10 Authentification et sécurisation 61
10.1 Enjeux de sécurité en GraphQL . . . . . . . . . . . . . . . . . . . . . . . . 61
10.2 Authentification JWT avec Spring Security . . . . . . . . . . . . . . . . . . 63
10.2.1 Dépendances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
10.2.2 Service JWT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
10.2.3 Configuration Spring Security . . . . . . . . . . . . . . . . . . . . . 65
ii
TABLE DES MATIÈRES
11 Tests 71
11.1 Stratégie de tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
11.2 Tests unitaires des services . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
11.3 Tests d’intégration avec GraphQlTester . . . . . . . . . . . . . . . . . . . . 73
11.4 Tests des mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
11.5 Tests avec HttpGraphQlTester (E2E) . . . . . . . . . . . . . . . . . . . . . 76
iii
Table des figures
iv
Chapitre 1
Introduction à GraphQL
— Over-fetching : les APIs REST renvoient souvent plus de données que nécessaire,
gaspillant de la bande passante.
— Under-fetching : une seule vue peut nécessiter plusieurs appels REST pour récupérer
toutes les données requises.
— Évolution rapide des clients : les équipes front-end et mobile avaient besoin de
pouvoir demander exactement les données nécessaires sans attendre des modifications
côté serveur.
1
CHAPITRE 1. INTRODUCTION À GRAPHQL
2
1.3. GRAPHQL VS REST : COMPARAISON DÉTAILLÉE
1. Point d’entrée unique : toutes les requêtes passent par /graphql, contrairement à
REST qui multiplie les endpoints.
3. Résolveurs découplés : chaque champ peut avoir son propre résolveur, permettant
d’agréger des données de multiples sources.
4. Indépendance des clients : chaque client peut demander exactement les données
dont il a besoin.
La comparaison entre GraphQL et REST est essentielle pour comprendre quand uti-
liser chaque approche.
Figure 1.3 – Comparaison REST vs GraphQL : nombre de requêtes et précision des données
3
CHAPITRE 1. INTRODUCTION À GRAPHQL
Attention
GraphQL ne remplace pas REST dans tous les cas. REST reste préférable pour les
APIs simples, le cache HTTP natif, ou les services de téléchargement de fichiers.
GraphQL excelle lorsque les clients ont des besoins de données variés et complexes.
1 query {
2 user ( id : " 1 " ) {
3 name
4 email
5 posts {
6 title
7 createdAt
8 }
9 }
10 }
4
1.4. LES TROIS OPÉRATIONS FONDAMENTALES
1 mutation {
2 createUser ( input : {
3 name : " Alice Dupont "
4 email : " alice@example . com "
5 }) {
6 id
7 name
8 }
9 }
1 subscription {
2 messageAdded ( roomId : " general " ) {
3 content
4 author {
5 name
6 }
7 createdAt
8 }
9 }
5
Chapitre 2
1 scalar DateTime
2 scalar Date
3 scalar URL
6
2.3. TYPES OBJETS
4 scalar Long
5 scalar BigDecimal
1 @Configuration
2 public class ScalarConfig {
3
4 @Bean
5 public RuntimeWiringConfigurer runtimeWiringConfigurer () {
6 return wiringBuilder -> wiringBuilder
7 . scalar ( ExtendedScalars . DateTime )
8 . scalar ( ExtendedScalars . Date )
9 . scalar ( ExtendedScalars . Url ) ;
10 }
11 }
1 type User {
2 id : ID !
3 name : String !
4 email : String !
5 age : Int
6 role : Role !
7 posts : [ Post !]!
8 createdAt : DateTime !
9 }
10
11 type Post {
12 id : ID !
13 title : String !
14 content : String !
15 published : Boolean !
16 author : User !
7
CHAPITRE 2. LE SYSTÈME DE TYPES GRAPHQL
23 type Comment {
24 id : ID !
25 text : String !
26 author : User !
27 post : Post !
28 createdAt : DateTime !
29 }
Note
Le point d’exclamation ! indique qu’un champ est non-nullable (obligatoire).
String! signifie que la valeur ne peut jamais être null. [Post!]! signifie que la
liste est non-nulle ET que chaque élément est non-nul.
1 enum Role {
2 ADMIN
3 MODERATOR
4 USER
5 GUEST
6 }
7
8 enum PostStatus {
9 DRAFT
10 PUBLISHED
11 ARCHIVED
12 }
13
14 enum SortOrder {
15 ASC
16 DESC
8
2.5. TYPES INTERFACES
17 }
Correspondance Java :
1 interface Node {
2 id : ID !
3 }
4
5 interface Timestamped {
6 createdAt : DateTime !
7 updatedAt : DateTime
8 }
9
9
CHAPITRE 2. LE SYSTÈME DE TYPES GRAPHQL
23 updatedAt : DateTime
24 }
3 type Query {
4 search ( term : String !) : [ SearchResult !]!
5 }
Côté client, on utilise des fragments inline pour sélectionner les champs selon le type
retourné :
1 query {
2 search ( term : " GraphQL " ) {
3 ... on User {
4 name
5 email
6 }
7 ... on Post {
8 title
9 content
10 }
11 ... on Comment {
12 text
13 }
14 }
15 }
10
2.8. MODIFICATEURS DE TYPES
1 input CreateUserInput {
2 name : String !
3 email : String !
4 password : String !
5 role : Role = USER
6 }
7
8 input UpdateUserInput {
9 name : String
10 email : String
11 age : Int
12 }
13
14 input PostFilter {
15 status : PostStatus
16 authorId : ID
17 tag : String
18 search : String
19 }
Attention
Les types input ne peuvent pas contenir de champs qui référencent des types objets.
Ils ne peuvent contenir que des scalaires, des enums et d’autres types input. C’est
une contrainte de conception de GraphQL.
Syntaxe Signification
11
Chapitre 3
Spring for GraphQL est le projet officiel de l’écosystème Spring pour l’intégration de
GraphQL. Il s’appuie sur graphql-java, le moteur GraphQL de référence en Java, et
fournit une intégration native avec Spring Boot.
12
3.2. CRÉATION DU PROJET
13
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE
1 <? xml version = " 1.0 " encoding = " UTF -8 " ? >
2 < project xmlns = " http: // maven . apache . org / POM /4.0.0 "
3 xmlns:xsi = " http: // www . w3 . org /2001/ XMLSchema - instance "
4 xsi:schemaLocation = " http: // maven . apache . org / POM /4.0.0
5 https: // maven . apache . org / xsd / maven -4.0.0. xsd " >
6 < modelVersion > 4.0.0 </ modelVersion >
7
14
3.2. CRÉATION DU PROJET
15
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE
spring :
application :
name : graphql - demo
datasource :
url : jdbc : postgresql :// localhost :5432/ graphql_db
username : postgres
password : postgres
driver - class - name : org . postgresql . Driver
jpa :
hibernate :
ddl - auto : update
show - sql : true
properties :
hibernate :
format_sql : true
dialect : org . hibernate . dialect . PostgreSQLDialect
graphql :
graphiql :
enabled : true
path : / graphiql
schema :
printer :
enabled : true
path : / graphql
server :
port : 8080
Conseil
GraphiQL est une interface web interactive pour tester vos requêtes Gra-
phQL. En activant [Link], vous pouvez y accéder à
[Link] C’est un outil indispensable pendant le dévelop-
pement.
16
3.4. STRUCTURE DU PROJET
1 # Scalaires personnalises
2 scalar DateTime
3
6 type User {
7 id : ID !
8 name : String !
9 email : String !
10 role : Role !
11 posts : [ Post !]!
12 createdAt : DateTime !
13 }
14
15 type Post {
16 id : ID !
17 title : String !
18 content : String !
19 status : PostStatus !
20 author : User !
21 comments : [ Comment !]!
22 createdAt : DateTime !
23 updatedAt : DateTime
24 }
25
26 type Comment {
27 id : ID !
28 text : String !
29 author : User !
17
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE
30 post : Post !
31 createdAt : DateTime !
32 }
33
36 enum Role {
37 ADMIN
38 MODERATOR
39 USER
40 }
41
42 enum PostStatus {
43 DRAFT
44 PUBLISHED
45 ARCHIVED
46 }
47
50 input CreateUserInput {
51 name : String !
52 email : String !
53 password : String !
54 role : Role = USER
55 }
56
57 input UpdateUserInput {
58 name : String
59 email : String
60 }
61
62 input CreatePostInput {
63 title : String !
64 content : String !
65 status : PostStatus = DRAFT
66 }
67
68 input AddCommentInput {
69 postId : ID !
70 text : String !
18
3.5. DÉFINITION DU SCHÉMA GRAPHQL
71 }
72
75 type UserPage {
76 content : [ User !]!
77 totalElements : Int !
78 totalPages : Int !
79 currentPage : Int !
80 hasNext : Boolean !
81 }
82
85 type Query {
86 # Utilisateurs
87 users ( page : Int = 0 , size : Int = 10) : UserPage !
88 user ( id : ID !) : User
89 me : User
90
91 # Posts
92 posts ( status : PostStatus ) : [ Post !]!
93 post ( id : ID !) : Post
94
95 # Recherche
96 search ( term : String !) : [ SearchResult !]!
97 }
98
99 type Mutation {
100 # Utilisateurs
101 createUser ( input : CreateUserInput !) : User !
102 updateUser ( id : ID ! , input : UpdateUserInput !) : User !
103 deleteUser ( id : ID !) : Boolean !
104
105 # Posts
106 createPost ( input : CreatePostInput !) : Post !
107 publishPost ( id : ID !) : Post !
108
109 # Commentaires
110 addComment ( input : AddCommentInput !) : Comment !
111 }
19
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE
112
1 @Entity
2 @Table ( name = " users " )
3 @Data
4 @NoArgsConstructor
5 @AllArgsConstructor
6 @Builder
7 public class User {
8
9 @Id
10 @GeneratedValue ( strategy = GenerationType . IDENTITY )
11 private Long id ;
12
20
3.6. CRÉATION DES ENTITÉS JPA
32 @PrePersist
33 protected void onCreate () {
34 this . createdAt = LocalDateTime . now () ;
35 }
36 }
1 @Entity
2 @Table ( name = " posts " )
3 @Data
4 @NoArgsConstructor
5 @AllArgsConstructor
6 @Builder
7 public class Post {
8
9 @Id
10 @GeneratedValue ( strategy = GenerationType . IDENTITY )
11 private Long id ;
12
21
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE
32
35 @PrePersist
36 protected void onCreate () {
37 this . createdAt = LocalDateTime . now () ;
38 }
39
40 @PreUpdate
41 protected void onUpdate () {
42 this . updatedAt = LocalDateTime . now () ;
43 }
44 }
1 @Entity
2 @Table ( name = " comments " )
3 @Data
4 @NoArgsConstructor
5 @AllArgsConstructor
6 @Builder
7 public class Comment {
8
9 @Id
10 @GeneratedValue ( strategy = GenerationType . IDENTITY )
11 private Long id ;
12
22
3.6. CRÉATION DES ENTITÉS JPA
27 @PrePersist
28 protected void onCreate () {
29 this . createdAt = LocalDateTime . now () ;
30 }
31 }
23
Chapitre 4
Queries et résolveurs
Lorsqu’une requête GraphQL arrive sur le serveur, elle passe par plusieurs étapes : par-
sing de la requête, validation contre le schéma, exécution via les résolveurs, et assemblage
de la réponse.
Chaque champ dans une requête GraphQL est résolu par un résolveur. Les résolveurs
forment une chaîne hiérarchique :
24
4.2. CHAÎNE DE RÉSOLUTION DES CHAMPS
25
CHAPITRE 4. QUERIES ET RÉSOLVEURS
1 @Repository
2 public interface UserRepository extends JpaRepository < User , Long > {
3
1 @Repository
2 public interface PostRepository extends JpaRepository < Post , Long > {
3
8 List < Post > findByAuthorIdIn ( List < Long > authorIds ) ;
9
26
4.4. COUCHE SERVICE
1 @Service
2 @RequiredArgsConstructor
3 public class UserService {
4
8 public Page < User > getUsers ( int page , int size ) {
9 return userRepository . findAll (
10 PageRequest . of ( page , size , Sort . by ( " createdAt " ) .
descending () )
11 );
12 }
13
27
CHAPITRE 4. QUERIES ET RÉSOLVEURS
40
Annotation Rôle
1 @Controller
2 @RequiredArgsConstructor
3 public class UserController {
28
4.5. CONTRÔLEURS GRAPHQL (RÉSOLVEURS)
8 @QueryMapping
9 public UserPage users ( @Argument int page , @Argument int size ) {
10 Page < User > userPage = userService . getUsers ( page , size ) ;
11 return new UserPage (
12 userPage . getContent () ,
13 ( int ) userPage . getTotalElements () ,
14 userPage . getTotalPages () ,
15 userPage . getNumber () ,
16 userPage . hasNext ()
17 );
18 }
19
20 @QueryMapping
21 public Optional < User > user ( @Argument Long id ) {
22 return userService . getUserById ( id ) ;
23 }
24
1 @Controller
2 @RequiredArgsConstructor
3 public class PostController {
4
9 @QueryMapping
10 public List < Post > posts ( @Argument PostStatus status ) {
11 if ( status != null ) {
12 return postService . getPostsByStatus ( status ) ;
29
CHAPITRE 4. QUERIES ET RÉSOLVEURS
13 }
14 return postService . getAllPosts () ;
15 }
16
17 @QueryMapping
18 public Optional < Post > post ( @Argument Long id ) {
19 return postService . getPostById ( id ) ;
20 }
21
Conseil
Avec @SchemaMapping, Spring for GraphQL appelle le résolveur uniquement si le
client demande le champ en question. Si la requête ne demande pas les posts d’un
User, le résolveur getPosts() ne sera jamais appelé. C’est l’un des avantages clés de
GraphQL.
1 query {
2 user ( id : " 1 " ) {
3 name
4 email
5 role
6 posts {
7 title
8 status
30
4.6. EXEMPLES DE REQUÊTES ET RÉPONSES
9 createdAt
10 }
11 }
12 }
Réponse JSON :
{
" data " : {
" user " : {
" name " : " Alice Dupont " ,
" email " : " alice@example . com " ,
" role " : " ADMIN " ,
" posts " : [
{
" title " : " Introduction a GraphQL " ,
" status " : " PUBLISHED " ,
" createdAt " : " 2024 -01 -15 T10 :30:00 "
},
{
" title " : " Spring Boot avance " ,
" status " : " DRAFT " ,
" createdAt " : " 2024 -02 -20 T14 :00:00 "
}
]
}
}
}
31
Chapitre 5
Mutations
1 @Controller
2 @RequiredArgsConstructor
3 public class UserMutationController {
4
7 @MutationMapping
8 public User createUser ( @Argument CreateUserInput input ) {
32
5.2. IMPLÉMENTATION DES MUTATIONS
12 @MutationMapping
13 public User updateUser ( @Argument Long id ,
14 @Argument UpdateUserInput input ) {
15 return userService . updateUser ( id , input ) ;
16 }
17
18 @MutationMapping
19 public boolean deleteUser ( @Argument Long id ) {
20 return userService . deleteUser ( id ) ;
21 }
22 }
1 @Controller
2 @RequiredArgsConstructor
3 public class PostMutationController {
4
7 @MutationMapping
8 public Post createPost ( @Argument CreatePostInput input ,
9 @AuthenticationPrincipal UserDetails
user ) {
10 return postService . createPost ( input , user . getUsername () ) ;
11 }
12
13 @MutationMapping
14 public Post publishPost ( @Argument Long id ) {
15 return postService . publishPost ( id ) ;
16 }
17
18 @MutationMapping
19 public Comment addComment ( @Argument AddCommentInput input ,
20 @AuthenticationPrincipal UserDetails
user ) {
21 return postService . addComment ( input , user . getUsername () ) ;
22 }
23 }
33
CHAPITRE 5. MUTATIONS
34
5.5. SERVICE POSTSERVICE COMPLET
4 String name ,
5
14 Role role
15 ) {}
1 @Configuration
2 public class GraphQLConfig {
3
4 @Bean
5 public RuntimeWiringConfigurer runtimeWiringConfigurer () {
6 return wiringBuilder -> wiringBuilder
7 . scalar ( ExtendedScalars . DateTime ) ;
8 }
9 }
1 @Service
2 @RequiredArgsConstructor
3 public class PostService {
4
35
CHAPITRE 5. MUTATIONS
24 @Transactional
25 public Post createPost ( CreatePostInput input , String
authorEmail ) {
26 User author = userRepository . findByEmail ( authorEmail )
27 . orElseThrow (() -> new UserNotFoundException (
28 " Auteur non trouve "
29 ));
30
42 @Transactional
43 public Post publishPost ( Long id ) {
44 Post post = postRepository . findById ( id )
45 . orElseThrow (() -> new PostNotFoundException (
46 " Post non trouve : " + id
47 ));
48 post . setStatus ( PostStatus . PUBLISHED ) ;
49 return postRepository . save ( post ) ;
50 }
36
5.6. EXEMPLES DE MUTATIONS
51
52 @Transactional
53 public Comment addComment ( AddCommentInput input , String email )
{
54 Post post = postRepository . findById ( input . postId () )
55 . orElseThrow (() -> new PostNotFoundException (
56 " Post non trouve : " + input . postId ()
57 ));
58 User author = userRepository . findByEmail ( email )
59 . orElseThrow (() -> new UserNotFoundException (
60 " Utilisateur non trouve "
61 ));
62
1 mutation {
2 createUser ( input : {
3 name : " Bob Martin "
4 email : " bob@example . com "
5 password : " secureP@ss123 "
6 role : MODERATOR
7 }) {
8 id
9 name
10 email
11 role
12 createdAt
13 }
37
CHAPITRE 5. MUTATIONS
14 }
1 mutation {
2 createPost ( input : {
3 title : " Mon premier article "
4 content : " Contenu de l ' article ... "
5 status : DRAFT
6 }) {
7 id
8 title
9 status
10 }
11 }
12
13 # Puis publication
14 mutation {
15 publishPost ( id : " 1 " ) {
16 id
17 title
18 status
19 updatedAt
20 }
21 }
38
Chapitre 6
1 query {
2 posts {
39
CHAPITRE 6. PROBLÈME N+1 ET DATALOADER
3 title
4 author {
5 name
6 }
7 }
8 }
40
6.2. SOLUTION : @BATCHMAPPING
1 @Controller
2 public class PostController {
3
1 @Controller
2 @RequiredArgsConstructor
3 public class PostController {
4
41
CHAPITRE 6. PROBLÈME N+1 ET DATALOADER
1 @Controller
2 @RequiredArgsConstructor
3 public class UserController {
4
42
6.4. COMPARAISON DES PERFORMANCES
Attention
Le problème N+1 est souvent invisible pendant le développement avec de petits jeux
de données. Activez toujours [Link]-sql=true en développement pour
détecter les requêtes excessives. En production, utilisez les métriques pour surveiller
le nombre de requêtes SQL par requête GraphQL.
43
Chapitre 7
{
" data " : null ,
" errors " : [
44
7.2. EXCEPTIONS PERSONNALISÉES
{
" message " : " Utilisateur non trouve " ,
" locations " : [ { " line " : 2 , " column " : 3 } ] ,
" path " : [ " user " ] ,
" extensions " : {
" classification " : " NOT_FOUND " ,
" code " : " USER_NOT_FOUND "
}
}
]
}
45
CHAPITRE 7. GESTION DES ERREURS
25
7.3 DataFetcherExceptionResolver
Spring for GraphQL permet de personnaliser la gestion des erreurs via un DataFetcherExceptionReso
1 @Component
2 public class GraphQLExceptionHandler
3 implements DataFetcherExceptionResolverAdapter {
4
5 @Override
6 protected GraphQLError resolveToSingleError (
7 Throwable ex , DataFetchingEnvironment env ) {
8
46
7.4. ERREURS PARTIELLES
23
{
" data " : {
" user " : {
47
CHAPITRE 7. GESTION DES ERREURS
Conseil
Les erreurs partielles sont une fonctionnalité puissante de GraphQL : même si un
résolveur échoue, le client reçoit les données des autres champs. C’est particulièrement
utile pour les dashboards où certaines sections peuvent être indépendantes.
48
Chapitre 8
Pagination et filtrage
1 type Query {
2 users ( page : Int = 0 , size : Int = 10) : UserPage !
3 posts ( page : Int = 0 , size : Int = 10 ,
4 filter : PostFilter ) : PostPage !
5 }
6
7 type UserPage {
8 content : [ User !]!
9 totalElements : Int !
10 totalPages : Int !
11 currentPage : Int !
12 hasNext : Boolean !
13 hasPrevious : Boolean !
14 }
15
16 type PostPage {
17 content : [ Post !]!
49
CHAPITRE 8. PAGINATION ET FILTRAGE
18 totalElements : Int !
19 totalPages : Int !
20 currentPage : Int !
21 hasNext : Boolean !
22 }
1 @Controller
2 @RequiredArgsConstructor
3 public class UserController {
4
7 @QueryMapping
8 public UserPage users ( @Argument int page ,
9 @Argument int size ) {
10 Page < User > result = userService . getUsers ( page , size ) ;
11
1 type Query {
2 usersConnection (
3 first : Int
4 after : String
50
8.3. PAGINATION CURSOR (RELAY-STYLE)
5 last : Int
6 before : String
7 ): UserConnection !
8 }
9
10 type UserConnection {
11 edges : [ UserEdge !]!
12 pageInfo : PageInfo !
13 totalCount : Int !
14 }
15
16 type UserEdge {
17 node : User !
18 cursor : String !
19 }
20
21 type PageInfo {
22 hasNextPage : Boolean !
23 hasPreviousPage : Boolean !
24 startCursor : String
25 endCursor : String
26 }
1 @Controller
2 @RequiredArgsConstructor
3 public class UserConnectionController {
4
7 @QueryMapping
8 public UserConnection usersConnection (
9 @Argument Integer first ,
10 @Argument String after ) {
11
51
CHAPITRE 8. PAGINATION ET FILTRAGE
18 . findByIdGreaterThanOrderByIdAsc (
19 afterId , PageRequest . of (0 , limit + 1)
20 );
21 } else {
22 users = userRepository
23 . findAllByOrderByIdAsc (
24 PageRequest . of (0 , limit + 1)
25 );
26 }
27
52
8.4. FILTRAGE ET TRI
59 );
60 return Long . parseLong (
61 decoded . replace ( " cursor : " , " " )
62 );
63 }
64 }
1 input PostFilter {
2 status : PostStatus
3 authorId : ID
4 search : String
5 createdAfter : DateTime
6 createdBefore : DateTime
7 }
8
9 input PostSort {
10 field : PostSortField !
11 order : SortOrder = ASC
12 }
13
14 enum PostSortField {
15 TITLE
16 CREATED_AT
17 UPDATED_AT
18 }
19
20 type Query {
21 posts ( filter : PostFilter , sort : PostSort ,
22 page : Int = 0 , size : Int = 10) : PostPage !
23 }
1 @Service
2 @RequiredArgsConstructor
3 public class PostService {
4
53
CHAPITRE 8. PAGINATION ET FILTRAGE
12 if ( filter != null ) {
13 if ( filter . status () != null ) {
14 spec = spec . and (( root , query , cb ) ->
15 cb . equal ( root . get ( " status " ) , filter . status () )
16 );
17 }
18 if ( filter . authorId () != null ) {
19 spec = spec . and (( root , query , cb ) ->
20 cb . equal ( root . get ( " author " ) . get ( " id " ) ,
21 filter . authorId () )
22 );
23 }
24 if ( filter . search () != null ) {
25 String pattern = " % " + filter . search ()
26 . toLowerCase () + " % " ;
27 spec = spec . and (( root , query , cb ) ->
28 cb . or (
29 cb . like ( cb . lower ( root . get ( " title " ) ) ,
30 pattern ) ,
31 cb . like ( cb . lower ( root . get ( " content " ) ) ,
32 pattern )
33 )
34 );
35 }
36 }
37
54
8.4. FILTRAGE ET TRI
55
Chapitre 9
56
9.3. IMPLÉMENTATION AVEC REACTOR (FLUX)
spring :
graphql :
websocket :
path : / graphql
connection - init - timeout : 30 s
1 @Service
2 public class PostEventPublisher {
3
57
CHAPITRE 9. SUBSCRIPTIONS (TEMPS RÉEL)
1 @Controller
2 @RequiredArgsConstructor
3 public class SubscriptionController {
4
7 @SubscriptionMapping
8 public Flux < Post > postPublished () {
9 return eventPublisher . getPostPublishedStream () ;
10 }
11
12 @SubscriptionMapping
13 public Flux < Comment > commentAdded ( @Argument Long postId ) {
14 return eventPublisher
15 . getCommentAddedStream ( postId ) ;
16 }
17 }
1 @Service
2 @RequiredArgsConstructor
3 public class PostService {
4
8 @Transactional
9 public Post publishPost ( Long id ) {
10 Post post = postRepository . findById ( id )
11 . orElseThrow (() -> new PostNotFoundException (
12 " Post non trouve "
13 ));
14 post . setStatus ( PostStatus . PUBLISHED ) ;
15 Post saved = postRepository . save ( post ) ;
16
58
9.5. CÔTÉ CLIENT
20 return saved ;
21 }
22
23 @Transactional
24 public Comment addComment ( AddCommentInput input ,
25 String email ) {
26 // ... creation du commentaire ...
27 Comment saved = commentRepository . save ( comment ) ;
28
32 return saved ;
33 }
34 }
1 subscription {
2 postPublished {
3 id
4 title
5 author {
6 name
7 }
8 createdAt
9 }
10 }
11
12 subscription {
13 commentAdded ( postId : " 42 " ) {
14 id
15 text
16 author {
17 name
18 }
59
CHAPITRE 9. SUBSCRIPTIONS (TEMPS RÉEL)
19 }
20 }
Note
Les subscriptions utilisent le protocole graphql-transport-ws. Les clients JavaScript
populaires comme Apollo Client et urql supportent nativement ce protocole.
60
Chapitre 10
Authentification et sécurisation
61
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION
62
10.2. AUTHENTIFICATION JWT AVEC SPRING SECURITY
10.2.1 Dépendances
63
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION
1 @Service
2 public class JwtService {
3
64
10.2. AUTHENTIFICATION JWT AVEC SPRING SECURITY
1 @Configuration
2 @EnableWebSecurity
3 @EnableMethodSecurity ( prePostEnabled = true )
4 @RequiredArgsConstructor
5 public class SecurityConfig {
6
10 @Bean
65
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION
28 @Bean
29 public PasswordEncoder passwordEncoder () {
30 return new BCryptPasswordEncoder () ;
31 }
32
33 @Bean
34 public AuthenticationManager authenticationManager (
35 AuthenticationConfiguration config )
36 throws Exception {
37 return config . getAuthenticationManager () ;
38 }
39 }
1 @Component
2 @RequiredArgsConstructor
3 public class JwtAuthenticationFilter
4 extends OncePerRequestFilter {
5
66
10.2. AUTHENTIFICATION JWT AVEC SPRING SECURITY
9 @Override
10 protected void doFilterInternal (
11 HttpServletRequest request ,
12 HttpServletResponse response ,
13 FilterChain chain ) throws ServletException ,
14 IOException {
15 String authHeader = request
16 . getHeader ( " Authorization " ) ;
17
18 if ( authHeader == null
19 || ! authHeader . startsWith ( " Bearer " ) ) {
20 chain . doFilter ( request , response ) ;
21 return ;
22 }
23
67
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION
49 }
50 }
1 @Controller
2 @RequiredArgsConstructor
3 public class AuthController {
4
9 @MutationMapping
10 public AuthPayload login ( @Argument String email ,
11 @Argument String password ) {
12 authManager . authenticate (
13 new UsernamePasswordAuthenticationToken (
14 email , password
15 )
16 );
17
1 @Controller
2 @RequiredArgsConstructor
3 public class AdminController {
68
10.5. PROTECTION CONTRE LES ABUS
7 @MutationMapping
8 @PreAuthorize ( " hasRole ( ' ADMIN ') " )
9 public boolean deleteUser ( @Argument Long id ) {
10 return userService . deleteUser ( id ) ;
11 }
12
13 @QueryMapping
14 @PreAuthorize ( " isAuthenticated () " )
15 public User me ( @AuthenticationPrincipal
16 UserDetails userDetails ) {
17 return userService
18 . getUserByEmail ( userDetails . getUsername () )
19 . orElseThrow () ;
20 }
21 }
1 @Configuration
2 public class GraphQLSecurityConfig {
3
4 @Bean
5 public Instrumentation maxQueryDepthInstrumentation () {
6 return new MaxQueryDepthInstrumentation (10) ;
7 }
8
9 @Bean
10 public Instrumentation maxQueryComplexity () {
11 return new MaxQueryComplexityInstrumentation (200) ;
12 }
13 }
69
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION
1 @Component
2 public class RateLimitInterceptor
3 implements WebGraphQlInterceptor {
4
8 @Override
9 public Mono < WebGraphQlResponse > intercept (
10 WebGraphQlRequest request ,
11 Chain chain ) {
12 String clientIp = request . getHeaders ()
13 . getFirst ( "X - Forwarded - For " ) ;
14
70
Chapitre 11
Tests
4 @Mock
71
CHAPITRE 11. TESTS
7 @Mock
8 private PasswordEncoder passwordEncoder ;
9
10 @InjectMocks
11 private UserService userService ;
12
13 @Test
14 void createUser_shouldCreateSuccessfully () {
15 // Given
16 var input = new CreateUserInput (
17 " Alice " , " alice@test . com " , " password " , Role . USER
18 );
19 when ( userRepository . existsByEmail ( " alice@test . com " ) )
20 . thenReturn ( false ) ;
21 when ( passwordEncoder . encode ( " password " ) )
22 . thenReturn ( " encoded " ) ;
23 when ( userRepository . save ( any ( User . class ) ) )
24 . thenAnswer ( inv -> {
25 User u = inv . getArgument (0) ;
26 u . setId (1 L) ;
27 return u ;
28 }) ;
29
30 // When
31 User result = userService . createUser ( input ) ;
32
33 // Then
34 assertThat ( result . getName () ) . isEqualTo ( " Alice " ) ;
35 assertThat ( result . getEmail () )
36 . isEqualTo ( " alice@test . com " ) ;
37 verify ( userRepository ) . save ( any ( User . class ) ) ;
38 }
39
40 @Test
41 void createUser_duplicateEmail_shouldThrow () {
42 var input = new CreateUserInput (
43 " Bob " , " exists@test . com " , " pass " , null
44 );
45 when ( userRepository . existsByEmail ( " exists@test . com " ) )
72
11.3. TESTS D’INTÉGRATION AVEC GRAPHQLTESTER
46 . thenReturn ( true ) ;
47
48 assertThatThrownBy (
49 () -> userService . createUser ( input )
50 ) . isInstanceOf ( DuplicateEmailException . class ) ;
51 }
52 }
1 @SpringBootTest
2 @AutoConfigureGraphQlTester
3 class UserControllerIntegrationTest {
4
5 @Autowired
6 private GraphQlTester graphQlTester ;
7
8 @Autowired
9 private UserRepository userRepository ;
10
11 @BeforeEach
12 void setUp () {
13 userRepository . deleteAll () ;
14 User user = User . builder ()
15 . name ( " Alice " )
16 . email ( " alice@test . com " )
17 . password ( " encoded " )
18 . role ( Role . USER )
19 . build () ;
20 userRepository . save ( user ) ;
21 }
22
23 @Test
24 void queryUser_shouldReturnUser () {
25 graphQlTester . document ( " " "
26 query {
27 user ( id : "1 " ) {
73
CHAPITRE 11. TESTS
28 name
29 email
30 role
31 }
32 }
33 """)
34 . execute ()
35 . path ( " user . name " ) . entity ( String . class )
36 . isEqualTo ( " Alice " )
37 . path ( " user . email " ) . entity ( String . class )
38 . isEqualTo ( " alice@test . com " )
39 . path ( " user . role " ) . entity ( String . class )
40 . isEqualTo ( " USER " ) ;
41 }
42
43 @Test
44 void queryUsers_shouldReturnPage () {
45 graphQlTester . document ( " " "
46 query {
47 users ( page : 0 , size : 10) {
48 content {
49 name
50 }
51 totalElements
52 hasNext
53 }
54 }
55 """)
56 . execute ()
57 . path ( " users . totalElements " )
58 . entity ( Integer . class ) . isEqualTo (1)
59 . path ( " users . hasNext " )
60 . entity ( Boolean . class ) . isEqualTo ( false )
61 . path ( " users . content [0]. name " )
62 . entity ( String . class ) . isEqualTo ( " Alice " ) ;
63 }
64 }
74
11.4. TESTS DES MUTATIONS
1 @SpringBootTest
2 @AutoConfigureGraphQlTester
3 class MutationIntegrationTest {
4
5 @Autowired
6 private GraphQlTester graphQlTester ;
7
8 @Test
9 void createUser_shouldReturnNewUser () {
10 graphQlTester . document ( " " "
11 mutation {
12 createUser ( input : {
13 name : " Bob "
14 email : " bob@test . com "
15 password : " securePass123 "
16 }) {
17 id
18 name
19 email
20 role
21 }
22 }
23 """)
24 . execute ()
25 . path ( " createUser . name " ) . entity ( String . class )
26 . isEqualTo ( " Bob " )
27 . path ( " createUser . role " ) . entity ( String . class )
28 . isEqualTo ( " USER " ) ;
29 }
30
31 @Test
32 void createUser_invalidEmail_shouldReturnError () {
33 graphQlTester . document ( " " "
34 mutation {
35 createUser ( input : {
36 name : " Test "
37 email : " invalid - email "
38 password : " pass "
39 }) {
75
CHAPITRE 11. TESTS
40 id
41 }
42 }
43 """)
44 . execute ()
45 . errors ()
46 . satisfy ( errors -> {
47 assertThat ( errors ) . isNotEmpty () ;
48 assertThat ( errors . get (0) . getMessage () )
49 . contains ( " validation " ) ;
50 }) ;
51 }
52 }
1 @SpringBootTest ( webEnvironment =
2 SpringBootTest . WebEnvironment . RANDOM_PORT )
3 class E2EGraphQLTest {
4
5 @Autowired
6 private HttpGraphQlTester . Builder <? > builder ;
7
8 @Test
9 void authenticatedQuery_shouldWork () {
10 String token = obtainJwtToken () ;
11
76
11.5. TESTS AVEC HTTPGRAPHQLTESTER (E2E)
23 """)
24 . execute ()
25 . path ( " me . name " ) . entity ( String . class )
26 . isEqualTo ( " Alice " ) ;
27 }
28 }
77
Chapitre 12
# Phase de build
FROM eclipse - temurin :21 - jdk AS build
WORKDIR / app
COPY pom . xml .
COPY src ./ src
RUN ./ mvnw clean package - DskipTests
78
12.2. CONTAINERISATION AVEC DOCKER
# Phase de runtime
FROM eclipse - temurin :21 - jre
WORKDIR / app
COPY -- from = build / app / target /*. jar app . jar
EXPOSE 8080
ENTRYPOINT [" java " , " - jar " , " app . jar "]
db :
image : postgres :16 - alpine
environment :
POSTGRES_DB : graphql_db
POSTGRES_USER : postgres
POSTGRES_PASSWORD : postgres
ports :
- " 5432:5432 "
volumes :
- pgdata :/ var / lib / postgresql / data
redis :
image : redis :7 - alpine
ports :
- " 6379:6379 "
volumes :
79
CHAPITRE 12. DÉPLOIEMENT ET ARCHITECTURE AVANCÉE
pgdata :
Dans une architecture microservices, GraphQL peut servir de gateway unifié. Deux
approches principales existent :
80
12.4. MONITORING ET OBSERVABILITÉ
3 < artifactId > spring - boot - starter - actuator </ artifactId >
4 </ dependency >
5 < dependency >
6 < groupId > io . micrometer </ groupId >
7 < artifactId > micrometer - registry - prometheus </ artifactId >
8 </ dependency >
management :
endpoints :
web :
exposure :
include : health , metrics , prometheus
metrics :
tags :
application : graphql - demo
spring :
graphql :
schema :
introspection :
enabled : false # Desactiver en production !
1 @Component
2 @RequiredArgsConstructor
3 public class GraphQLMetricsInterceptor
4 implements WebGraphQlInterceptor {
5
8 @Override
9 public Mono < WebGraphQlResponse > intercept (
10 WebGraphQlRequest request , Chain chain ) {
11
81
CHAPITRE 12. DÉPLOIEMENT ET ARCHITECTURE AVANCÉE
17
82
12.6. CONCEPTION DU SCHÉMA : BONNES PRATIQUES
83
CHAPITRE 12. DÉPLOIEMENT ET ARCHITECTURE AVANCÉE
2. Utiliser des types Input : toujours passer les arguments de mutation via un type
input.
3. Retourner les objets modifiés : une mutation doit retourner l’objet créé ou modifié.
4. Préférer les non-null : utiliser ! par défaut, rendre nullable uniquement si nécessaire.
1 type User {
2 id : ID !
3 name : String !
4 fullName : String !
5 username : String @deprecated (
6 reason : " Utiliser 'name ' a la place "
7 )
8 }
Domaine Recommandation
84
12.7. RÉSUMÉ DES BONNES PRATIQUES
Conseil
Pour aller plus loin, explorez DGS Framework (Netflix), qui offre des fonctionnalités
avancées comme la génération de code à partir du schéma, ou Apollo Federation pour
les architectures microservices.
85