Cloud & DevOps

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

by Baptiste Leulliette 21 janvier 2026

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.

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Kind } = require("graphql")

function capitalize(str) {
  const firstChar = str.charAt(0)

  const remaining = str.slice(1, str.length)

  return `${firstChar.toUpperCase()}${remaining}`
}

const template = ({ operationName, hasVariables }) => {
  const variables = hasVariables ? `variables: SchemaTypes.${operationName}QueryVariables,\n` : ""
  const signature = `${variables}options?: import('nuxt/app').AsyncDataOptions<SchemaTypes.${operationName}Query>`

  const queryVariablesTyping = hasVariables ? `, SchemaTypes.${operationName}QueryVariables` : ""
  const queryTyping = `SchemaTypes.${operationName}Query${queryVariablesTyping}`
  const queryPayloadVariables = hasVariables ? "\nvariables," : ""

  return `export function use${operationName}AsyncData(${signature}) {
  const { client } = VueApolloComposable.useApolloClient();

  return useAsyncData(() => client.query<${queryTyping}>(
    {
      query: ${operationName}Document,${queryPayloadVariables}
    },
  ).then(result => result.data),
    options
  )
}
\n
`
}

const plugin = {
  plugin(_schema, documents, _config, _info) {
    const buffer = ["\n"]

    for (const { document } of documents) {
      const operation = document?.definitions[0]

      if (operation.kind !== Kind.OPERATION_DEFINITION || operation.operation !== "query") continue

      const operationName = capitalize(operation?.name?.value ?? "")
      const hasVariables = (operation.variableDefinitions?.length ?? 0) > 0

      buffer.push(template({ hasVariables, operationName }))
    }

    return buffer.join("\n")
  }
}

module.exports = plugin

 

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.

Le Backend-for-frontend en pratique avec GraphQL

Dans nos environnements de développement

Il est nécessaire, après chaque changement, que ce soit dans le schéma du Backend-for-frontend ou dans les documents du frontend d’exécuter à nouveau la commande de Codegen. Il a donc fallu adapter nos séquences de lancement des projets : au lieu de lancer les services directement, nous avons utilisé une librairie JS Concurrently, qui permet en une seule commande npm de lancer autant de services que nécessaire. Cela permet d’exploiter les fonctionnalités de watch de la librairie de Codegen dans nos environnements de développement. Par exemple ici avec notre frontend :

    "generate:types:watch": "graphql-codegen --config codegen.yml --watch '../backend/src/**/*.graphql'",
   "nuxt": "nuxt dev",
   "dev": "concurrently --raw --kill-others \"npm run generate:types:watch\" \"npm run nuxt\"",

 

En l’occurrence, pour notre frontend, dès qu’une modification de schéma dans le Backend-for-frontend est effectuée, le Codegen s’exécute et va générer le nouveau fichier d’introspection, nos fichiers de typages et le fichier d’opérations. Ces fichiers étant présents sur le disque, vont permettre de déclencher le HMR de Nuxt et d’avoir la page à jour sans aucune autre action de la part du développeur.

Et en production alors ?

Peu de changements sont nécessaires, vu qu’il s’agit d’un mono-repository et des Dockerfiles qui permettent de build nos images docker poussées sur un registry. Nous avons procédé à quelques ajustements dans les Dockerfile en ajoutant deux lignes de build supplémentaires :

FROM base AS build


USER root


COPY . /usr/app/frontend
COPY ../design-system /usr/app/design-system
COPY ../backend /usr/app/backend


WORKDIR /usr/app/frontend


RUN cd ../backend && npm ci && npm run generate:types && \
   cd ../design-system && npm ci && npm run build && \
   cd ../frontend && npm ci && npm run generate:types && npm run build

L’avantage du Backend-for-Frontend : uniformisation et gain de temps avec GraphQL

On a vu comment générer le typage TypeScript à partir du schéma, comment générer de l’introspection, ce qui nous permet d’obtenir de l’auto-completion dans nos fichiers .graphql. 

Cela donne un avantage notable dans la rapidité et la sécurité de la communication de vos applications vers le Back-for-frontend, grâce à la génération des opérations à partir des queries et mutations. Nous concernant, il permet un gain de temps, une uniformisation et une continuité dans l’implémentation de la communication frontend/Backend-for-frontend sur nos projets.

Cette approche amène le schéma au centre de votre application. C’est de celui-ci que va découler tout le reste de votre application. Ainsi sécurisé, il devient une fondation solide et efficace pour le reste du projet.

Baptiste Leulliette

Baptiste Leulliette

Expert technique

Développeur JS, je suis passionné par les technologies d'aujourd'hui et de demain. Je porte aussi un grand intérêt à l'aspect DevOps, principalement avec AWS.

Commentaires

Ajouter un commentaire

Votre commentaire sera modéré par nos administrateurs

Vous avez un projet ? Nos équipes répondent à vos questions

Contactez-nous