Let's talk

Comment GraphQL Codegen améliore l’expérience de développement frontend avec un Backend-for-frontend

Dans un précédent article, je vous parlais des avantages d’avoir un Backend-for-frontend dans votre architecture composable. Mais qu’en est-il de la communication de vos différents frontaux avec celui-ci ? Que faire pour avoir une expérience de développement la plus fluide possible ? Il s’agit de questions que nous nous sommes posées et voici comment nous avons travaillé sur ces problématiques, notamment en utilisant GraphQL. 

La première étape a été de se demander de quoi avions-nous besoin, que ce soit côté Backend-for-frontend ou côté frontend. 

Voici ce qu’il en ressort : 

  • Des typages TypeScript cohérents, associés à l’état du Backend-for-frontend à un instant T, disponibles à la fois dans le Backend-for-frontend, à la fois dans les frontaux.
  • La génération des queries entre le frontend et le Backend-for-frontend permettant l’inférence des types dans le frontend depuis le Backend-for-frontend. 

Ainsi dans cet objectif, nous avons utilisé GraphQL Codegen avec quelques plugins. 

Génération des typages TypeScript dans le Backend-for-frontend avec GraphQL 

Dans le Backend-for-frontend ainsi que dans le frontend, nous avons utilisé deux plugins de la librairie de Codegen, les plugins « typescript » et “typescript-resolvers” dans un fichier de configuration, nommé “codegen.yml” :

# This configuration file tells GraphQL Code Generator how
# to generate types based on our schema.
schema: "./**/*.graphql"
generates:
 ./src/__generated__/resolvers-types.ts:
   plugins:
     - "typescript"
     - "typescript-resolvers"
   config:
     useIndexSignature: true
     contextType: "../context.js#ApolloContext"

En décortiquant ce fichier, on retrouve un auto-discover de tous les types .graphql qui tiennent l’intégralité du schéma. Le Codegen va générer dans un fichier "resolver-types.ts" tous les typages TypeScript associés au schéma. De la configuration supplémentaire est nécessaire, notamment, ajouter le boolean “useIndexSignature”. Vous trouverez davantage d’informations via ce lien GitHub. 

À noter que les fichiers générés, le sont dans un dossier __generated__ ajouté au .gitignore.

Pour finir, nous avons ajouté une commande NPM pour exécuter le Codegen :

graphql-codegen --config codegen.yml

Voici un exemple de schéma GhraphQL et le typages TypeScript généré :

type Accordion {
  id: ID!
  header: String!
  content: String!
  slug: String!
}

Et cela pour l’intégralité des types GraphQL qui sont présents dans les fichiers .graphql.

Dans les faits, à quoi ça sert ?

Tous nos datasources et dataloaders passent à travers des mappers pour s’assurer que nos données sont compatibles avec les schémas du Backend-for-frontend.
C’est principalement dans ces mappers, en spécifiant le retour des fonctions dans nos mappers avec le typage généré, que les typages TypeScript sont utilisés et valident les réponses des resolvers.
Un autre cas d’usage est l’utilisation des types dans notre frontend pour propager des interfaces de props ou autre.

Côté frontend : génération du schéma d’introspection du Backend-for-frontend avec GraphQL

Passons côté frontend. Dans l’objectif de fournir l’autocompletion dans nos fichiers .graphql qui serviront à générer nos opérations, il faut générer l’introspection de l’intégralité du schéma GraphQL. Il est donc possible d’utiliser le plugin « introspection« . En initiant le fichier de Codegen, côté frontend, dans notre application Nuxt, cela donne ceci :

const config = {
 overwrite: true,
 schema: '../backend/**/*.graphql',
 documents: 'modules/graphql/**/*.graphql',
 generates: {
   'graphql.schema.json': {
     plugins: ['introspection'],
   },
 },
}
export default config

Nous y retrouvons : La lecture de tous les fichiers .graphql du backend, qui fournissent le schéma dans son ensemble, en exploitant au maximum l’approche mono-repo de nos projets. Tous les fichiers .graphql du front, nommés “documents”. Les différents fichiers de codegen qui vont être générés, notamment le fichier d’introspection. En exécutant le Codegen côté frontend, on se retrouve donc avec l’intégralité du schéma dans un fichier JSON, exploitable par le .graphqlrc qui permet de fournir l’autocomplete dans nos “documents”. Cela permet donc d’implémenter très facilement les queries et mutations dans nos fichiers .graphql côté frontend. A noter encore une fois, que ce fichier est ajouté dans le .gitignore pour éviter des conflits de versioning.

Côté frontend : génération des fichiers d’opérations du Backend-for-fronted via GraphQL


Une opération, c’est une query ou une mutation. Au lieu de définir nos définitions de query dans des fichiers .js ou .ts, nous allons les écrire dans des fichiers .graphql qui serviront de bases pour la génération des opérations.

Nous utilisons 3 plugins qui, à partir de ces fichiers .graphql, vont produire toutes les queries et mutations nécessaires.

Le Codegen côté frontend ressemble donc, finalement, à ceci :

const config = {
 overwrite: true,
 schema: '../backend/**/*.graphql',
 documents: 'modules/graphql/**/*.graphql',
 generates: {
   'graphql.schema.json': {
     plugins: ['introspection'],
   },
   'modules/graphql/schema.ts': {
     plugins: ['typescript', 'typescript-operations'],
   },
   'modules/graphql/operations.ts': {
     preset: 'import-types',
     presetConfig: {
       typesPath: './schema',
       importTypesNamespace: 'SchemaTypes',
     },
     config: {
       documentMode: 'documentNodeImportFragments',
       dedupeFragments: true,
       identifierName: 'Operation',
       useConsts: true,
       withCompositionFunctions: true,
       vueCompositionApiImportFrom: 'vue',
       useTypeImports: true,
     },
     plugins: ['typescript-vue-apollo', 'named-operations-object', 'codegen-plugins/use-async-data-query.cjs'],
   },
 },
}

En utilisant les plugins “typescript-vue-apollo”, “named-operations-object” et un troisième, celui-ci custom (nous y reviendrons un peu plus bas), on arrive à générer toutes les opérations possibles dans un fichier “operations.ts”. En prenant pour l’exemple une query d’une de nos applications, on se retrouve avec ce code généré :

query homepage($locale: String!) {
 homePage(locale: $locale) {
   id
   title
   description
   content {
     ... on ButtonBlock {
       __typename
       id
       component
       style
       button {
         label
         link
         noFollow
       }
     }
   }
 }
}
/**
* __useHomepageQuery__
*
* To run a query within a Vue component, call `useHomepageQuery` and pass it any options that fit your needs.
* When your component renders, `useHomepageQuery` returns an object from Apollo Client that contains result, loading and error properties
* you can use to render your UI.
*
* @param variables that will be passed into the query
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
*
* @example
* const { result, loading, error } = useHomepageQuery({
*   locale: // value for 'locale'
* });
*/
export function useHomepageQuery(variables: SchemaTypes.HomepageQueryVariables | VueCompositionApi.Ref<SchemaTypes.HomepageQueryVariables> | ReactiveFunction<SchemaTypes.HomepageQueryVariables>, options: VueApolloComposable.UseQueryOptions<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables>> = {}) {
 return VueApolloComposable.useQuery<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables>(HomepageDocument, variables, options);
}
export function useHomepageLazyQuery(variables?: SchemaTypes.HomepageQueryVariables | VueCompositionApi.Ref<SchemaTypes.HomepageQueryVariables> | ReactiveFunction<SchemaTypes.HomepageQueryVariables>, options:
VueApolloComposable.UseQueryOptions<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables>> = {}) {
 return VueApolloComposable.useLazyQuery<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables>(HomepageDocument, variables, options);
}
export type HomepageQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<SchemaTypes.HomepageQuery, SchemaTypes.HomepageQueryVariables>;
export const MarketVillageLayoutDocument = { ... }

Et cela s’applique donc pour l’intégralité des queries et mutations que nous avons implémentées dans notre frontend. Pour les adeptes de Nuxt et Vue, les queries générées le sont pour une application Vue mais ne rentrent pas dans le cycle de vie d’un composant Nuxt pour le rendu SSR. C’est pour cette raison que Lukka Blois a développé un plugin Codegen, qui permet de générer les opérations dans des asyncData qui respectent le cycle de vie.

Ainsi, en plus des “use[MyOperationName]Query” et des “use[MyOperationName]LazyQuery” qui sont produits, nous avons aussi un “use[MyOperationName]AsyncData” qui nous permet de respecter le cycle de vie de nos composants.

Une fois n’est pas coutume, ce fichier “operation.ts” n’est pas versioné pour éviter les problématiques de versioning.