Partager les types entre front & back

Disponible dans 1 jour

Devenir premium
Résumé Support

Dans cette vidéo, je vous propose de voir les différentes approches qu'il est possible d'utiliser pour partager les types lorsqu'on décide de séparer le backend du frontend.

Sommaire

  • 00:00 Introduction
  • 01:06 Types côté serveur : tRPC
  • 05:24 Types via un contrat : ts-rest
  • 08:33 Types via OpenAPI
  • 12:51 Types via schéma GraphQL
  • 19:27 Types via Protobuf
  • 21:14 Server functions
  • 25:17 Conclusion

Types côté serveur : tRPC

Avec tRPC, c'est le serveur qui est responsable du typage. On définit un routeur côté backend, avec les méthodes disponibles, leurs entrées et leurs retours. Ce routeur sert ensuite de type pour le frontend qui aura une structure similaire (mêmes méthodes, paramètres et retours...).

Le client front se charge de la couche HTTP et le typage assure que les types côté client correspondent à ce qui a été défini côté serveur.

Faire évoluer un type serveur

En cas de modification, on change juste la logique côté serveur et le front remontera automatiquement les erreurs lors de la vérification de type car il importe les types provenant du serveur.

Avantages

  • L'expérience côté frontend est très simple : les méthodes du serveur sont directement disponibles, avec l'autocomplétion et les types.
  • La synchronisation est immédiate : une évolution du routeur fait remonter les appels incompatibles.
  • tRPC peut aussi générer des intégrations, notamment pour React Query.

Limites

  • Le backend doit adopter la structure et l'écosystème de tRPC.
  • Le frontend est lui aussi lié à cet écosystème et à son évolution dans le temps.
  • La structure fonctionne mieux avec une structure monorepo.

Types via un contrat : ts-rest

L'approche par contrat place la responsabilité du typage dans un package partagé qui servira de modèle pour la conception du backend (et de base pour les types front). Avec ts-rest par exemple, le contrat est défini via des schémas Zod qui représentent les paramètres, les réponses, les URLs et les statuts. Il ne s'agit donc plus de déduire le client depuis l'implémentation serveur : le contrat décrit ce que le serveur devra respecter et ce que le client pourra utiliser.

Le serveur implémente ce contrat et le client l'importe également pour créer ses appels typés. Le contrat devient le point de rencontre entre les deux équipes.

Faire évoluer un type serveur

Si on souhaite faire évoluer notre application, on commence d'abord par éditer le contrat pour représenter notre changement. Ensuite, on peut corriger le code côté backend et frontend pour suivre cette nouvelle structure.

Avantages

  • Le contrat est une source de vérité explicite, indépendante de l'implémentation serveur.
  • Les équipes frontend et backend peuvent travailler à partir de ce contrat dès le début d'une fonctionnalité. Le frontend n'a pas besoin d'attendre que le serveur ait terminé son implémentation pour recevoir un nouveau type.
  • Les schémas Zod servent aussi à valider les données à l'exécution.

Limites

  • Cette approche ajoute une étape de conception et reste centrée sur TypeScript : il faut partager le package et adopter l'architecture de l'outil.
  • Comme avec tRPC, le serveur et le client doivent accepter le cadre imposé par l'écosystème choisi (TypeScript de bout en bout).

Types via OpenAPI

OpenAPI est une spécification qui décrit une API HTTP dans un fichier JSON ou YAML. Ce fichier est responsable du contrat : il détaille les routes, leurs paramètres, les corps de requête et les réponses. Il peut être écrit à la main ou généré depuis le code serveur (avec hono (TS), elysia (TS), APIPlatform (PHP), FastAPI (python))

Le frontend ne partage pas nécessairement le même langage ou le même dépôt que le backend. Il consomme le document OpenAPI pour générer un client. Par exemple, Orval permet de produire les types TypeScript et du code client.

Faire évoluer un type serveur

En cas de changement de signature de l'API, il faudra mettre à jour le document OpenAPI (manuellement ou automatiquement suivant la situation). Le frontend pourra alors relancer la régénération automatique.

Avantages et limites

  • Le contrat est portable : le serveur peut utiliser le langage et l'architecture de son choix tant que le document OpenAPI reste synchronisé.
  • Le même fichier peut servir à générer de la documentation, par exemple avec Swagger, et des clients pour plusieurs environnements.
  • La génération du client rend les évolutions de routes et de modèles visibles dans le frontend.
  • Certains modèles complexes, notamment des objets discriminés dont la forme dépend d'une propriété, sont plus difficiles à exprimer.
  • Si le document est maintenu à la main ou n'est pas régénéré, API et types frontend peuvent se désynchroniser. Cette discipline est indispensable.

Types via un schéma GraphQL

Avec GraphQL, la responsabilité du contrat est portée par le schéma SDL. Il expose les types, les entrées, les requêtes et les mutations que le serveur autorise. Le frontend complète ce contrat avec ses opérations : chacune sélectionne précisément les champs dont l'interface a besoin.

query { countries { code name continent { name } } }

Cette flexibilité rend le typage difficile, mais des outils peuvent lire le schéma et les opérations pour générer les types de réponse. C'est par exemple le cas de GraphQL Code Generator, qui génère les types des résolveurs côté serveur, ainsi que les types de paramètres et de réponse côté client.

Faire évoluer un type serveur

En cas de mise à jour, on commence par modifier le schéma, puis les résolveurs et leur validation côté serveur. Côté frontend, on peut relancer la génération de code après avoir modifié nos requêtes pour récupérer les bonnes informations.

Avantages

  • Le frontend choisit les champs dont il a besoin, ce qui convient aux interfaces dont les besoins en données varient fortement selon l'écran.
  • Le schéma rend les possibilités de l'API explicites et les opérations donnent un type précis à chaque requête.

Limites

  • Cette flexibilité demande une implémentation et un outillage plus lourds, côté serveur comme côté client.
  • Le serveur doit contrôler soigneusement la profondeur et le coût des requêtes. Une requête qui charge de nombreuses relations peut surcharger la base de données.
  • Il n'existe pas une manière unique de gérer GraphQL : les bibliothèques clientes et serveur introduisent leurs propres conventions et leur propre structure.

Types via Protobuf

Les Protobufs reposent sur un fichier .proto responsable de la définition. Il décrit les messages échangés et les services disponibles dans une syntaxe dédiée. À partir de cette définition, on génère les types serveur à implémenter et le code client nécessaire pour appeler le service.

Le principe est donc proche d'OpenAPI : le fichier de définition relie les deux côtés, au lieu de partager directement l'implémentation TypeScript.

Faire évoluer un type serveur

On modifie le fichier .proto pour faire évoluer les messages ou les services. Il faut ensuite adapter les implémentations et les appels qui ne correspondent plus à la nouvelle définition.

Avantages

  • Cette approche fonctionne bien avec certains langages, notamment Go, et permet de partager une définition entre client et serveur.
  • Protobuf et gRPC sont avant tout pensés pour des échanges binaires rapides, même s'il est possible de passer par du JSON selon les protocoles utilisés.

Limites

  • La syntaxe et le modèle de définition sont spécifiques, et l'approche est moins flexible qu'OpenAPI dans le retour d'expérience présenté ici.
  • Elle est moins naturelle dans un environnement Node.js que les autres solutions de cet article, mais peut être pertinente avec Go, Kotlin, Dart ou Swift.

Server functions

Les Server Functions, utilisées notamment par TanStack Start, rapprochent le frontend et le backend dans une même application. La fonction déclarée côté serveur est responsable du typage : sa signature, son validateur et son type de retour sont importés et utilisés directement côté interface.

Au build, le framework transforme cet import en appel RPC HTTP. Depuis un composant React, on appelle donc une fonction comme createUser({ data }) sans écrire de client HTTP à la main. Le framework transporte l'appel.

Faire évoluer un type serveur

Comme pour tRPC, une modification de la signature de la fonction entraîne automatiquement un changement des types associés.

Avantages

  • Le typage est continu entre la fonction serveur et son appel client, avec très peu de code d'interopérabilité à écrire.
  • Les fonctions peuvent aussi être appelées lors du rendu côté serveur pour charger des données dès le premier affichage.

Limites

  • La frontière entre frontend et backend est moins nette, puisque les deux vivent dans le même projet et dépendent du framework.
  • Cette approche implique une structure et un hébergement adaptés aux Server Functions. Pour exposer une API publique, on utilisera plutôt des Server Routes.
  • Elle reprend l'idée de tRPC, mais dans un package full-stack unique, ce qui peut être déstabilisant si l'on préfère séparer strictement les deux applications.

Choisir une approche

Aujourd'hui, il existe plein de solutions pour obtenir du typage de bout en bout et le choix dépendra de plusieurs facteurs :

  • Quel langage maîtrisez-vous / voulez-vous utiliser côté serveur ?
  • Quelle sera l'utilité de l'API en dehors du web ?
  • Quel niveau de dépendance à une structure tierce êtes-vous prêt à accepter ?