Construire et structurer une API GraphQL en Go

Construire et structurer une API GraphQL en Go


GraphQL est disponible depuis maintenant presque 2 ans et les applications qui l'utilisent se font toujours assez rare. Pourtant, cette implémentation proposée par Facebook offre de nombreuses possibilités que ne permet pas une API REST.

Introduction

L'objectif de cet article n'est pas de vous expliquer ce qu'est GraphQL, la documentation située à l'adresse http://graphql.org/learn l'explique déjà très bien !

Je me suis donc intéressé à construire une API GraphQL, et tant qu'à avoir une API performante, j'ai choisi le langage Go pour la développer, à l'aide de la librairie graphql-go (https://github.com/graphql-go/graphql).

Structure de fichiers de notre API

La première chose (et pas des moindres) à prendre en compte lorsque l'on souhaite développer une application est la structure de celle-ci.

En effet, notre API va être amenée à évoluer, nous allons avoir de plus en plus d'éléments à fournir à nos applications et peut-être allons-nous souhaiter ajouter des composants (pour sécuriser notre API, pour logger des informations, pour limiter le nombre de requêtes, etc ...).

Ainsi, voici l'arborescence que je vous propose pour notre API :

. ├── app │ ├── config.go │ ├── config.json │ └── config_test.go ├── security │ ├── security.go │ └── security_test.go ├── mutations │ ├── mutations.go │ ├── mutations_test.go │ ├── user.go │ └── user_test.go ├── queries │ ├── queries.go │ ├── queries_test.go │ ├── user.go │ └── user_test.go ├── types │ ├── role.go │ ├── role_test.go │ ├── user.go │ └── user_test.go └── main.go

Nous retrouvons ici :

  • "app/" qui comprendra tout ce qui sera nécessaire à notre application (API), principalement un fichier de configuration (JSON) config.json ainsi que le fichier Go permettant de charger ce JSON,
  • "security/" permettra de regrouper les classes liées à la sécurisation de notre API,
  • "mutations/" permettra de regrouper toutes les mutations GraphQL (modifications de données),
  • "queries/" permettra de regrouper toutes les requêtes GraphQL de sélection de données,
  • "types/" permettra de regrouper les structures Go utilisées lors de nos mutations ou requêtes.

Enfin, nous retrouvons bien sûr à la racine main.go qui est le point d'entrée de notre API. Nous allons d'ailleurs commencer dès maintenant à construire notre API !

Point d'entrée de l'API

Pour construire notre API, nous allons avoir besoin dans un premier temps d'importer le package "net/http" (car notre API GraphQL va être distribuée en HTTP) ainsi que les librairies graphql-go :

package main import ( "log" "net/http" "github.com/graphql-go/graphql" "github.com/graphql-go/handler" ) func main() { // Todo: Implement GraphQL handler http.Handle("/", httpHandler) log.Print("ready: listening...\n") http.ListenAndServe(":8383", nil) }

Vous remarquerez ici qu'il nous manque la variable httpHandler, qui sera en fait le handler HTTP GraphQL qui sera exécuté pour chaque requête sur "/". Aussi, nous précisons ici que nous allons écouter sur le port 8383, libre à vous de mettre celui que vous souhaitez.

Notre httpHandler va avoir besoin d'un schéma dans lequel nous allons spécifier deux points d'entrée : un pour les requêtes et un second pour les mutations :

schemaConfig := graphql.SchemaConfig{ Query: graphql.NewObject(graphql.ObjectConfig{ Name: "RootQuery", Fields: queries.GetRootFields(), }), Mutation: graphql.NewObject(graphql.ObjectConfig{ Name: "RootMutation", Fields: mutations.GetRootFields(), }), } schema, err := graphql.NewSchema(schemaConfig) if err != nil { log.Fatalf("Failed to create new schema, error: %v", err) } httpHandler := handler.New(&handler.Config{ Schema: &schema })

Dans le cas ou vous n'avez aucune modifications de données mais uniquement des requêtes de sélection, vous pouvez bien sûr supprimer la section concernant les mutations.

Ici, il nous manque queries.GetRootFields() ainsi que mutations.GetRootFields(). Ces méthodes vont nous permettre de définir toutes les queries et mutations que nous allons définir par la suite.

Plutôt que d'alourdir le fichier main.go, j'ai choisi de les déposer sous queries/queries.go et mutations/mutations.go.

Structures de données

Avant de commencer à écrire notre première requête, nous devons définir notre modèles de données.

Dans cet article, nous allons partir sur des données utilisateur ("user") avec un identifiant, un prénom et un nom. Cela donne nous pour notre fichier types/user.go :

package types import ( "github.com/graphql-go/graphql" ) // User type definition. type User struct { ID int `db:"id" json:"id"` Firstname string `db:"firstname" json:"firstname"` Lastname string `db:"lastname" json:"lastname"` } // UserType is the GraphQL schema for the user type. var UserType = graphql.NewObject(graphql.ObjectConfig{ Name: "User", Fields: graphql.Fields{ "id": &graphql.Field{Type: graphql.Int}, "firstname": &graphql.Field{Type: graphql.String}, "lastname": &graphql.Field{Type: graphql.String}, }, })

Nous avons ici définis deux choses :

  • Une structure Go, qui sera utilisée par notre base de données et afin de renvoyer les données de notre API au format JSON,
  • Un object UserType qui sera utilisé par notre API GraphQL afin d'indiquer les champs qui peuvent être retournés aux applications.

À l'aide de ce modèle de données, nous sommes maintenant prêts à construire notre première requête GraphQL !

Requêtes

Commençons par éditer le fichier queries/queries.go afin d'ajouter une requête user qui sera chargée de retourner nos données utilisateur :

package queries import ( "github.com/graphql-go/graphql" ) // GetRootFields returns all the available queries. func GetRootFields() graphql.Fields { return graphql.Fields{ "user": GetUserQuery(), } }

Nous avons donc ajoutés un nouveau champ à notre requête principale écrite précédemment (RootQuery) nommé user et qui fera appel à la fonction GetUserQuery().

Nous allons maintenant définir cette fonction et son comportement dans un fichier dédié sous queries/user.go :

package queries import ( "../types" "github.com/graphql-go/graphql" ) // GetUserQuery returns the queries available against user type. func GetUserQuery() *graphql.Field { return &graphql.Field{ Type: graphql.NewList(types.UserType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { var users []types.User // ... Implémenter la logique de base de données ici return users, nil }, } }

Notre première requête est prête : nous allons utiliser le type de données UserType, il ne vous reste plus qu'à implémenter la logique de retour de vos données !

Vous pouvez à cet endroit faire un appel à tout outil de stockage de vos données : bases de données relationnelles ou non, SQL ou non, fichier, mémoire, tout est envisageable.

Ajouter des relations à votre API

Imaginons maintenant que vous ayez des roles (pour gérer des accès à certaines ressources) associés à vos utilisateurs.

Vous pouvez également demander à votre API de retourner ceux-ci. Pour cela, nous allons commencer par implémenter une nouvelle structure Role ainsi qu'un nouveau type RoleType pour GraphQL.

Créez donc le fichier types/role.go avec le code suivant :

package types import ( "github.com/graphql-go/graphql" ) // Role type definition. type Role struct { ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` } // RoleType is the GraphQL schema for the user type. var RoleType = graphql.NewObject(graphql.ObjectConfig{ Name: "Role", Fields: graphql.Fields{ "id": &graphql.Field{Type: graphql.Int}, "name": &graphql.Field{Type: graphql.String}, }, })

Voilà qui est fait. Il faut maintenant que nous spécifions à notre UserType qu'il est possible d'obtenir les roles de l'utilisateur.

Pour cela, éditez le fichier types/user.go et ajoutez une nouvelle section graphql.Field vers votre RoleType :

var UserType = graphql.NewObject(graphql.ObjectConfig{ Name: "User", Fields: graphql.Fields{ // ... already defined fields "roles": &graphql.Field{ Type: graphql.NewList(RoleType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { var roles []Role // userID := params.Source.(User).ID // Implement logic to retrieve user associated roles from user id here. return roles, nil }, }, }, })

Notez ici que le Type spécifié est un graphql.NewList(RoleType) car nous allons retourner une liste de roles et non pas un seul role.

Pour effectuer votre requête, vous pouvez utiliser params.Source pour obtenir les informations de l'élément principal (ici, l'utilisateur) et ainsi obtenir vos données liées à cet utilisateur.

Enfin, ce qui est intéressant ici est que le requêtage de données (roles) sera effectué uniquement si le client effectuant la requête GraphQL demande à obtenir les roles.

Effectuer des appels à votre API

À partir de là, vous pouvez donc intéroger votre API avec la requête suivante :

curl -X POST -H 'Content-Type: application/json' -d '{"query": "query { users { id,firstname,lastname,roles{name} } }"}' http://localhost:8383/ {"data":{"user":[{"id":1,"firstname":"Vincent","lastname":"COMPOSIEUX","roles":[]}, ...]}}

Bien entendu, uniquement les champs demandés dans la requête vous seront retournés, c'est le principe.

GraphQL offre bien sûr des possibilités intéressantes au niveau des requêtes avec notamment des aliases, variables et fragments qui ne sont pas l'objectif de cet article mais je vous invite à faire un tour dans la documentation, ça se comprend très simplement facilement :

Mutations

Côté mutations, le fonctionnement est identique aux requêtes. Nous allons donc créer notre première mutation et vous allez voir que ça ressemble beaucoup aux queries.

Créez le fichier "mutations/mutations.go" et spécifions notre RootMutation avec notre fonction GetRootFields() :

package mutations import ( "github.com/graphql-go/graphql" ) // GetRootFields returns all the available mutations. func GetRootFields() graphql.Fields { return graphql.Fields{ "createUser": GetCreateUserMutation(), } }

Ici, nous allons créer une mutation pour ajouter un nouvel utilisateur dans notre base de données.

Déclarons donc maintenant la fonction GetCreateUserMutation() dans le fichier mutations/user.go :

package mutations import ( "../types" "github.com/graphql-go/graphql" ) // GetCreateUserMutation creates a new user and returns it. func GetCreateUserMutation() *graphql.Field { return &graphql.Field{ Type: types.UserType, Args: graphql.FieldConfigArgument{ "firstname": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "lastname": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { user := &types.User{ Firstname: params.Args["firstname"].(string), Lastname: params.Args["lastname"].(string), } // Add your user in database here return user, nil }, } }

Votre mutation est prête à être utilisée !

Comme vous pouvez le remarquer, nous avons ici ajoutés une section Args qui nous permet de définir des arguments à notre fonction, par exemple : createUser(firstname: "John", lastname: "Snow").

Il est ensuite possible de tester votre API en effectuant la requête HTTP suivante :

curl -X POST -H 'Content-Type: application/json' -d '{"query": "mutation { createUser(firstname: \"John\", lastname: \"Snow\") { id,firstname,lastname } }"}' http://localhost:8383

Vous pouvez bien sûr choisir d'obtenir en retour uniquement l'identifiant de l'utilisateur nouvellement créé.

Securité

La plupart de vos APIs ne sont certainement pas publiques, il vous faut donc y ajouter un composant de sécurité, et c'est ce que nous allons faire ici en intégrant une authentification JWT (https://jwt.io/).

Nous allons utiliser la librairie dgrijalva/jwt-go (https://github.com/dgrijalva/jwt-go) afin de simplifier l'intégration de JWT dans notre application.

Ajoutez simplement dans votre fichier security/security.go le contenu suivant :

package security import ( "fmt" "log" "net/http" jwt "github.com/dgrijalva/jwt-go" ) // Handle security middleware aims to implement a JWT authentication. func Handle(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenString := r.Header.Get("Authorization")[7:] // 7 corresponds to "Bearer " token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } var secret = "my-high-security-secret" // Prefer to store this secret in a configuration file return []byte(secret), nil }) if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { log.Printf("JWT Authenticated OK (app: %s)", claims["app"]) next.ServeHTTP(w, r) } }) }

Ici, nous récupérons le token reçu dans le header Authorization: Bearer xxx et allons l'utiliser pour le comparer avec notre secret.

Dans le cas ou le token est valide, l'application continuera à exécuter le handler HTTP, sinon, une erreur sera levée.

Pour utiliser ce composant de sécurité, il faut repasser sur notre fichier main.go afin d'importer le répertoire security et de modifier :

http.Handle("/", httpHandler)

en :

http.Handle("/", security.Handle(httpHandler))

Vous disposez maintenant d'une API GraphQL performante et sécurisée !

Conclusion

L'implémentation de GraphQL en Go est plutôt simple à prendre en main et les performances du langage permettent de construire une API performante.

Il nous est également possible de bien structurer celle-ci afin de séparer notamment les queries, les mutations et les autres composants.

Si vous voulez tester cette structure, les sources sont disponibles ici : https://github.com/eko/graphql-go-structure

Auteur(s)

Vincent Composieux

Vincent Composieux

Architecte passionné par les technologies web depuis de longues années, je pratique principalement du PHP (Symfony) / Javascript mais aussi du Python ou Golang.

Voir le profil

Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !

Notre équipe d'experts répond à toutes vos questions.

Nous contacter

Découvrez nos autres contenus dans le même thème

Livre blanc : L'architecture logicielle, tout ce que vous devez savoir

Livre blanc : L'architecture logicielle, tout ce que vous devez savoir

L'évolution des standards du web et de développement, couplée à la multiplication des technologies permettant de développer des applications web, des différents frameworks associés, des outils, ont généré le besoin de définir une architecture claire des projets.

Déployer un serveur MongoDB répliqué sur AWS avec Terraform et Ansible

Déployer un serveur MongoDB répliqué sur AWS avec Terraform et Ansible

J'ai récemment eu l'occasion de déployer un serveur MongoDB sur Amazon Web Services (AWS). Afin de limiter les problèmes de crash et de perte de données, celui-ci est également répliqué avec deux autres serveurs, idéalement dans une zone géographique différente pour assurer de la haute disponibilité.