Développe une application de mise en relation avec Remix.js, NestJS, Typescript, Turborepo
Développeur depuis plus de 4 ans, je t'aide à maîtriser Remix avec des formations de qualité sur Youtube. Profil LinkedIn
Pourquoi cette formation Remix.js + NestJS ?
En 4 ans de développement, je n'ai pas encore trouvé une stack qui me plaît. Il y a toujours un élément qui manque (une fonctionnalité, ou une limitation technique).
En tant que développeur fullstack, je souhaite bénéficier du meilleur des deux mondes.
Je souhaite utiliser une technologie :
- simple à utiliser
- qui me permet d'implémenter une fonctionnalité rapidement
- qui me permet d'avoir un contrôle total sur la logique, front comme back
Pourquoi Remix.js ?
Remix répond à mes attentes. C'est un framework frontend qui me permet d'utiliser Javascript et React pour créer des sites web performants et ergonomiques.
Cependant, il n'a pas suffisamment de maturité. Il manque plein de features, comme les middleware (qui sont très utiles pour ne pas recopier la même logique de protection des routes)
J'utilise donc NestJS comme serveur séparé jusqu'à présent.
Pourquoi NestJS ?
Ce framework Node.JS me permet d'utiliser Javascript pour configurer une base de donnée, des routes et toute la logique métier.
Ensuite, j'appelle chaque route dans Remix. Mais c'est sujet à beaucoup d'erreurs d'inattention, ou de perte de synchronisation. J'informe Remix des réponses API de NestJS en déclarant un schéma Zod, qui peut être erroné, et générer des erreurs.
Je perd donc pas mal de temps à :
- déclarer des schémas Zod
- réparer des bugs, erreurs d'inattention
- déclarer des méthodes pour appeler mes routes
Pourquoi un monorepo ?
Utiliser cette stack va t'éviter toutes ces erreurs ! Car tu peux intégrer ce serveur NestJS avec Remix. Cela remplace entièrement le serveur de Remix.
Voici les avantages :
- aucune duplication de code
- aucun schéma zod
- aucun bug de ce style à régler
C'est un gain de temps énorme. Les types seront partagés entre le serveur et le client. Tu pourras même utiliser les mêmes fonctions côté back et front.
Ressources utilisées
- Repository Github
- Documentation de Remix
- Documentation de NestJS
- Documentation de Turborepo
- Documentation de Prisma
- Formation vidéo sur YouTube
Présentation de la stack technique
Nous allons utiliser les technologies suivantes pour créer notre application :
Frontend
Backend
Librairies communes
Qualité de code
Déploiement
- Github Actions
- VPS (serveur hébergé) chez Hostinger
- Caddy Server
- Docker
Pré-requis
Vous devez au préalable avoir installé NodeJS sur votre machine :
Pour vérifier que NodeJS est bien installé, ouvrez un terminal et tapez la commande suivante :
Cette commande devrait vous renvoyer un numéro de version. Par exemple, v20.10.0
Ensuite, il faut télécharger l'outil CLI de NestJS pour générer le projet :
Configuration du projet
Commençons par créer un nouveau dossier qui va contenir nos deux projets.
Nous allons d'abord créer le projet NestJS, qui va se charger par la suite d'exécuter le code source de Remix.
Nous utiliserons npm
comme package manager.
Nous allons ensuite ouvrir le dossier stack-remix-nestjs
dans un éditeur de code (personnellement, j'utilise VSCode).
Configuration de NestJS
Une fois ouvert, notre interface ressemble à ceci
Nous pouvons supprimer le fichier src/app.controller.spec.ts
et le dossier test
car nous n'en aurons pas besoin.
Création du RemixController
À l'intérieur du fichier src
, nous allons créer un dossier nommé remix
et à l'intérieur de ce dossier, un fichier remix.controller.ts
.
Nous allons créer une route catch-all, qui va rediriger toutes les requêtes vers l'application Remix. (Nous verrons plus tard comment configurer Remix pour qu'il puisse gérer ces requêtes).
Nous allons installer une dépendance supplémentaire pour servir l'application Remix.
Configuration de l'adapter Remix
Nous allons installer la librairie @remix-run/express
dans notre projet NestJS.
Cette librairie va nous permettre de communiquer avec l'application Remix depuis NestJS.
Modifions le controlleur pour ajouter le handler de Remix.
N'oublions pas d'importer ce contrôleur dans le fichier app.module.ts
, pour qu'il soit détecté par NestJS.
Maintenant, on a besoin de générer le build
de l'application Remix pour le servir depuis NestJS.
Configuration de Remix
Création d'un nouveau projet Remix
Nous devons d'abord reculer d'un dossier pour créer le projet Remix, à la racine de stack-remix-nestjs
.
Nous appelons le projet frontend
. Et nous répondons Non
à la question Voulez-vous instancier un nouveau repository git ?
.
On devrait maintenant avoir un dossier frontend
à la racine de stack-remix-nestjs
.
Création d'un nouveau fichier index.cjs et index.d.cts
Attention. Nous allons créer un fichier
index.cjs
qui sera exécuté et interprété par l'application NestJS. Il utilise la syntaxe CommonJS, qui est compatible avec NestJS.
Ce fichier déclare trois fonctions que nous utiliserons dans l'application NestJS :
getPublicDir
: retourne le chemin du dossierbuild/client
getServerBuild
: retourne le moduleserver/index.js
de l'application RemixstartDevServer
: démarre le serveur de développement de Remix
Pour bénéficier de l'autocomplétion, nous devons créer un fichier index.d.cts
qui va déclarer les types de ces fonctions.
Configurer le point d'entrée de l'application Remix dans le fichier package.json
Après avoir créé ces deux fichiers, nous devons modifier le fichier package.json
de Remix pour que NestJS connaisse son point d'entrée (et sa déclaration de types).
C'est seulement après avoir ajouté cette instruction qu'on bénéficiera de l'auto-complétion du module
frontend
.
Voilà un package.json
complet.
Ce qui nous intéresse ici, ce sont les deux premières lignes :
_10 "main": "./index.cjs",_10 "types": "./index.d.cts",
Grâce à l'ajout de ces deux lignes, toute application qui importera le module @virgile/frontend
bénéficiera de l'autocomplétion des fonctions getPublicDir
, getServerBuild
et startDevServer
.
Nous avons terminé la configuration de ces deux projets pour l'instant. Nous allons maintenant configurer le monorepo pour qu'ils puissent communiquer entre eux.
Configuration du monorepo
Pour configurer le monorepo, nous avons besoin de reculer d'un dossier.
À l'intérieur du dossier stack-remix-nestjs
, nous allons instancier un nouveau fichier package.json
pour configurer notre monorepo.
Le fichier package.json
doit ressembler à ça :
Installation de Turbo
Nous allons utiliser une librairie appelée Turborepo
pour gérer notre monorepo.
Nous pouvons aussi l'installer en global pour bénéficier de la commande
turbo
dans le terminal.
Ensuite, nous allons déclarer nos deux projets comme des workspaces dans le fichier package.json
.
Le fichier devrait maintenant ressembler à ça :
Création du fichier turbo.json
Nous devons ensuite créer un fichier de configuration Turbo. Pour ce faire, nous pouvons créer un fichier nommé turbo.json
.
Ensuite, nous allons coller la configuration par défaut, qu'on retrouve sur la documentation officielle :
(Optionnel) Création d'un fichier .gitignore
Nous pouvons ajouter un fichier .gitignore
pour éviter de versionner les fichiers volumineux.
_10.turbo_10node_modules_10/dist_10/build_10/out
Création de pipelines avec Turborepo
Les pipelines permettent de définir des commandes que turbo va exécuter pour chaque projet.
Exemple : Nous allons créer une pipeline nommée dev
pour exécuter l'application NestJS en environnement de développement.
D'abord, nous devons faire une petite modification aux fichiers package.json
respectifs de chaque projet.
Nous allons ajouter trois commandes dans le fichier backend/package.json
de notre application NestJS.
Explication : La première commande va exécuter en parallèle les deux commandes suivantes.
dev:compile
va compiler le code source de NestJS en continu.
dev:watch
va surveiller les changements apportés au code et redémarrer le serveur à chaque modification.
Pour ce faire, nous devons également installer les dépendances nodemon
et npm-run-all
.
Ensuite, renommons la commande dev
par old-dev
dans le frontend/package.json
pour l'empêcher de s'exécuter.
Nous devons maintenant créer une pipeline dev
dans notre fichier de configuration turbo.json
Pour exécuter cette pipeline, nous devons lancer la commande turbo
dans le terminal, suivi du nom de la pipeline.
Nous allons créer une commande dans le fichier package.json
de la racine pour exécuter cette pipeline.
Renommer la librairie frontend pour l'importer dans le backend
Maintenant, lancer la commande npm run dev
dans le terminal exécutera l'application NestJS (qui s'occupera d'exécuter Remix.js).
On rencontre l'erreur suivante.
Pas de panique, c'est normal. Nous n'avons pas encore importé les méthodes définies dans le fichier frontend/index.cjs
.
Pour pouvoir importer ces méthodes, nous devons déclarer la librairie @virgile/frontend
dans le fichier backend/package.json
.
D'abord, renommons nos projets backend/package.json
et frontend/package.json
.
Ces noms vont nous permettre d'utiliser la librairie @virgile/frontend
dans notre application NestJS.
Après avoir fait ce changement, il nous suffit d'ajouter la dépendance @virgile/frontend
en tant que dependencies
dans le fichier backend/package.json
.
Attention. Comme nous venons de renommer notre librairie frontend
, nous devons nous assurer d'utiliser le même nom dans le fichier frontend/index.d.cts
Importer getServerBuild dans RemixController
Nous pouvons à présent importer la méthode getServerBuild
dans le fichier backend/src/remix/remix.controller.ts
.
Voici le fichier modifié :
Utiliser les méthodes getPublicDir et startDevServer
Il nous manque deux méthodes à utiliser avant de pouvoir utiliser Remix.js.
Nous allons importer les méthodes getPublicDir
et startDevServer
dans le fichier backend/src/main.ts
.
Nous exécutons la méthode startDevServer
, qui va démarrer le serveur de développement de Remix.js en utilisant Vite. On n'aura donc pas besoin d'exécuter deux serveurs en parallèle, NestJS s'occupe de tout.
Ensuite, nous déclarons le dossier public
de Remix.js comme dossier statique pour que NestJS puisse servir les fichiers statiques (comme les images, le favicon, le CSS ...).
Lancer le serveur de développement
Nous pouvons à présent lancer le serveur de développement en exécutant la commande npm run dev
dans le terminal.
Le serveur devrait démarrer sans erreur.
Ajouter Typescript et Eslint
Nous allons maintenant ajouter deux librairies à notre monorepo : Typescript
et Eslint
. Pour ce faire, nous allons créer un fichier de configuration pour chaque librairie, qu'on va ensuite réutiliser dans nos applications NestJS et Remix.js.
Nous allons nous inspirer de la configuration de Raphaël Moreau, un développeur Remix qui a créé un monorepo nommé Remix Galaxy
Nous allons donc créer un nouveau dossier nommé packages
à la racine de notre monorepo.
Il va contenir nos configurations Typescript et Eslint.
Configurer Typescript
Pour ajouter notre configuration Typescript, nous allons créer un nouveau dossier typescript-config
à l'intérieur de packages
.
Nous allons ensuite générer un fichier package.json
, pour télécharger les dépendances nécessaires.
Nous allons changer le nom pour qu'il ressemble au code ci-dessus :
_10{_10 "name": "@virgile/typescript-config",_10 "version": "0.0.0",_10 "private": true,_10 "license": "MIT",_10 "publishConfig": {_10 "access": "public"_10 },_10 "prettier": {}_10}
Nous allons ensuite créer un fichier base.json
pour y coller notre configuration globale Typescript.
Et voilà ! Nous pouvons maintenant importer cette configuration Typescript dans nos projet NestJS et Remix.js.
Il nous suffit d'ajouter la dépendance @virgile/typescript-config
dans les fichiers backend/package.json
et frontend/package.json
.
Pourquoi utiliser Typescript comme ça ? Pour nous éviter de répéter la même configuration dans chaque projet. Nous avons besoin de faire un dernier changement avant que la configuration soit détectée.
Nous devons déclarer cette configuration globale dans le fichier tsconfig.json
de chaque projet. Il nous suffit de rajouter l'instruction extends
avec le chemin relatif vers la configuration.
Configurer Eslint
Nous allons reproduire la même opération pour Eslint.
Créons un nouveau dossier packages/eslint-config
à la racine de notre monorepo.
Nous allons ensuite générer un fichier package.json
, pour télécharger les dépendances nécessaires.
Nous devons ensuite installer toutes les librairies relatives à nos règles Eslint.
Et nous allons également copier sa configuration globale.
Maintenant, ajoutons cette librairie comme dépendance de nos projets.
Nous devons ensuite extends
cette configuration dans les configurations Eslint de chaque projet.
J'ai finalement décidé de ne pas l'utiliser dans la configuration Eslint du backend. Je vous partage quand même la configuration.
Nous l'utilisons essentiellement pour notre application Remix !
Ajouter ces packages en tant que workspaces
Nous avons créé ces deux packages pour éviter de répéter la même configuration dans chaque projet. Nous devons maintenant les ajouter dans le fichier package.json
principal.
C'est l'instruction packages/*
qui va informer NodeJS et Turbo que chaque dossier à l'intérieur du dossier packages
(contenant un fichier packages.json
) est à considérer comme workspace.
Nous avons terminé la configuration de Typescript et Eslint. Avant de commencer à coder, il nous reste plus qu'à intégrer notre service NestJS dans notre application Remix.js.
Ajout d'une route Remix de démonstration
Le but de cette stack est de pouvoir bénéficier de la puissance de Remix.js tout en utilisant l'architecture modulaire de NestJS. La logique métier sera contenu dans les services de NestJS. Ce que nous voulons faire, c'est vérifier que l'intégration entre les deux projets fonctionne correctement. Pour ce faire, nous allons créer une route Remix qui va appeler une méthode de service NestJS.
Création d'une route Remix
Pour créer une route, il nous suffit d'ajouter un fichier dans le dossier frontend/app/routes
(ce dossier contient toutes les routes de notre application Remix).
Nous l'avions vu dans l'article 6 Routes à connaître si tu utilises Remix.
Pour créer une route, il nous suffit d'ajouter un fichier dans le dossier app/routes
, qui exporte une méthode loader (pour créer une API) ou un composant React par défaut (pour créer une vue).
Nous venons de créer une route API, communément appelée Ressource Route. Elle ne renvoie pas une vue car aucun composant React n'a été exporté par défaut. Mais elle renvoie une réponse JSON.
On peut lancer le projet à la racine avec la commande npm run dev
, et naviguer à l'URL localhost:3000/api
pour voir le résultat.
Victoire ! Notre réponse JSON s'affiche bien. Essayons maintenant d'afficher une vue en créant un composant React dans le même fichier.
Actualisons la page. Nous devrions voir le texte Hello
s'afficher.
Deuxième victoire ! Notre route Remix fonctionne correctement. Nous allons maintenant importer un service NestJS dans notre application Remix.
Ajout du RemixService (dans NestJS)
Il est temps de créer notre premier service NestJS. Il ne va pas contenir de logique métier à proprement parler, il nous permettra d'importer tous les autres services de notre application, qui eux la contiendront.
Créons un nouveau fichier remix.service.ts
dans le dossier backend/src/remix
.
Nous avons ajouté une méthode getHello
qui renvoie une chaîne de caractères. Nous allons ensuite appeler cette méthode dans Remix pour vérifier que tout fonctionne correctement.
N'oublions pas d'abord d'importer ce service dans le fichier app.module.ts
Pour pouvoir détecter la déclaration des types de notre application NestJS, nous devons modifier le fichier package.json
. (On avait fait la même chose pour Remix.js, déclarant le point d'entrée, le fichier index.cjs
)
Il nous suffit de rajouter ces deux lignes dans le fichier backend/package.json
.
Pourquoi pointe-t-on vers les fichiers compilés du serveur Remix ? Parce que ce sont les seuls fichiers que nous allons exécuter depuis notre application Remix. Pour générer ces fichiers, nous devons lancer la commande npm run build
à la racine de notre projet.
Comment utiliser ce service ? Nous allons l'importer dans notre controlleur RemixController
.
Nous allons ensuite le renvoyer au contexte de Remix, tel quel.
N'oublions pas de re-compiler ce code avec la commande npm run dev
, avant de passer à la suite.
De retour dans Remix.js
Voici notre première route, nommée api.tsx
. Pour récupérer le service envoyé par NestJS, nous devons ajouter des arguments à notre méthode loader (que Remix se charge de nous fournir)
Nous pouvons ensuite extraire l'object context
, qui contient toute information qu'on souhaite partager depuis le serveur NestJS.
Plus haut, notre fichier RemixController
renvoyait le service RemixService
dans le contexte, ainsi que la variable toto
.
Elles sont déjà accessibles dans Remix, bien qu'on ne possède pas encore l'autocomplétion.
Je ne peux plus me passer de l'autocomplétion. Nous allons donc l'ajouter.
Ajouter l'autocomplétion des méthodes de NestJS dans Remix
Pour bénéficier de l'autocomplétion, nous devons écrire une déclaration de module (ce que nous avions fait tout à l'heure dans le fichier frontend/index.d.cts
)
Cette fois, nous allons la copier directement dans le fichier frontend/root.tsx
Il nous suffit de rajouter cette syntaxe n'importe où dans le fichier frontend/root.tsx
Bien sûr, nous avons également besoin d'importer le type RemixService
, en haut du fichier.
Cela nous donne le fichier suivant :
Grâce à cet ajout, nous possédons maintenant l'autocomplétion dans le context
de Remix. Regardez:
Utiliser le service RemixService dans notre route
Nous pouvons maintenant utiliser le service RemixService
dans notre route api.tsx
. Nous allons appeler la méthode getHello
côté serveur (dans la méthode loader) pour bénéficier de sa valeur côté client.
Nous sommes donc en train de :
- Appeler une méthode NestJS côté serveur
- Renvoyer son résultat côté client
- Récupérer le résultat côté client dans notre composant React
Il nous reste plus qu'à configurer Turborepo ! Ensuite, nous pourrons passer au développement de notre application.
Configuration de Turborepo
Ajout des commandes typecheck et lint
Nous avons déjà ajouté le mot clé workspaces
dans le package.json
principal. Il informe NodeJS que nous utilisons un monorepo.
Maintenant, lorsque nous installons nos dépendances avec npm install
, NodeJS saura qu'il doit installer les dépendances de chaque projet.
Nous avions déjà ajouté la commande dev
aux scripts
du package.json
. Elle exécute la commande turbo dev
Pourquoi turbo dev
? Parce que nous avons défini une pipeline nommée dev
dans la configuration Turborepo (le fichier turbo.json
)
Pour rappel, voici son contenu
Nous allons rajouter deux autres pipelines à cette configuration, pour utiliser Eslint et Typescript.
Il nous suffit de rajouter ces deux clés typecheck
et lint
, qui prendront le même nom que les scripts
que l'on va ajouter à nos fichiers package.json
.
Pour exécuter ces pipelines, nous devons aussi les rajouter au fichier package.json
principal :
Maintenant, lorsque nous exécutons la commande npm run typecheck
, Turborepo va regarder la configuration de chaque projet (frontend/package.json
, backend/package.json
, packages/eslint-config/package.json
, packages/typescript-config/package.json
respectivements).
Si ces projets possèdent un script nommé typecheck
, Turbo va l'exécuter en parallèle. S'ils n'en possèdent pas, Turbo ne l'exécutera pas.
En lançant la commande npm run dev
à la racine du monorepo, on exécute le scripts
nommé dev
de chaque projet. Mais seul le projet backend
possède un script nommé dev
. C'est ce que nous souhaitons : Le serveur NestJS va se charger de lancer l'application Remix.js. Nous n'avons pas besoin d'avoir une commande dev
dans le package.json
du frontend. Si c'est le cas, nous devons la retirer.
Pour rappel, voici notre package.json
.
Il possède les commandes suivantes :
- Une commande
typecheck
, pour détecter les erreurs Typescript - Une commande
lint
pour détecter les erreurs Eslint (et appliquer les bonnes pratiques) - Une commande
build
pour construire l'application Remix.js
En environnement de développement, NestJS se charge de build le code source de Remix. Cependant en production, nous avons besoin de la commande build
de Remix pour compiler le code source du frontend.
Ajout de la commande build
Notre fichier de configuration turbo.json
contient déjà une commande build
. Mais elle est erronée (nous avons laissé la configuration par défaut).
Effectivement, l'instruction outputs
défini un tableau de dossiers contenant le code source compilé, une fois la commande build
exécutée. Mais le chemin n'est pas bon.
Notre application NestJS compile bien son code dans le dossier dist
, le bon chemin est donc backend/dist/**
.
Notre application Remix compile son code dans le dossier build
, le bon chemin est donc frontend/build/**
.
De plus, avant de pouvoir compiler le code source de NestJS, nous avons besoin de compiler l'application Remix. (Car NestJS utilise le build de Remix, dans son main.ts
. Il est donc important de build le code source de Remix, avant de build l'application NestJS.)
Comme notre backend dépend du frontend (nous avions ajouté l'instruction ci-dessous), Turborepo saura qu'il faut d'abord compiler le frontend avant de compiler le backend.
Ajoutons la clé "dependsOn": ["^build"]
au fichier de configuration pour que Turbo en soit informé.
Maintenant, exécuter la commande npm run build
à la racine du monorepo va compiler le code source de l'application Remix, puis celui de l'application NestJS.
Cependant, nous avons un problème dans le terminal. Comme nous utilisons un monorepo, nous avons besoin d'utiliser un chemin absolu dans le fichier de configuration vite.config.ts
Modification de la configuration Vite de Remix
Jusqu'à présent, nous n'avions pas touché au fichier de configuration de notre application Remix.
On peut supprimer l'intégralité du fichier vite.config.ts
, et le remplacer par le code suivant :
Nous ajoutons une clé resolve, qui contient l'instruction preserveSymlinks: true
. Cela permet à Vite de résoudre les liens symboliques, et de ne pas les suivre. Cette option est nécessaire pour le bon fonctionnement de notre application.
Comme nous utilisons la librairie remix-flat-routes
, la déclaration appDir: resolve(__dirname, "app")
est également nécessaire pour que Vite puisse trouver les routes de notre application depuis NestJS.
Le reste de la configuration est propre à Remix.
Réessayons un build de notre application. Le problème devrait être résolu.
Compilation de l'application NestJS avec Typescript
Nous avons presque terminé cette configuration. Cependant, une petite modification du package.json
de notre application NestJS s'impose.
Qu'avons-nous changé ?
- La commande
dev
exécute deux commandes en parallèle (grâce à la librairienpm-run-all
) - La commande
dev:compile
compile le code source de l'application NestJS - La commande
dev:watch
lance le serveur NestJS en mode watch - La commande
prebuild
supprime les fichiers de compilation précédents (pour éviter un bug). La commandeprebuild
se lance automatiquement lorsque l'on exécute la commandebuild
- La commande
typecheck
permet de détecter les erreurs Typescript - La commande
lint
permet de détecter les erreurs Eslint (et appliquer les bonnes pratiques) - La commande
format
permet de formater le code source avec Prettier
On teste notre application
Avant de versionner notre code sur Github, testons chacune des commandes une par une.
Erreurs de type
Détectons les erreurs de type. Normalement, il ne devrait pas en détecter.
Bonnes pratiques
Détectons les bonnes pratiques avec Eslint. Il se peut qu'il nous signale quelques erreurs.
Si c'est le cas et que ça concerne les dossiers build
ou node_modules
, il est possible d'ignorer ces deux fichiers en créant un fichier .eslintignore
à la racine du projet.
Pour désactiver le cache sur les commandes typecheck
et lint
, il suffit de désactiver l'option dans la configuration Turborepo, en rajoutant l'instruction cache: false
dans les pipelines.
Relançons la commance pour bénéficier d'une analyse fiable, sans cache.
Build de l'application
Compilons le code source de l'application. Normalement, nous avons résolu toutes les erreurs.
Lancement de l'application
Nous avons oublié d'ajouter la commande start
au fichier package.json
de notre application. Ajoutons-là tout de suite.
Cette commande va exécuter la commande start
de notre backend/package.json
On peut maintenant la lancer à la racine du projet.
Héberger le code source sur Github
Avant d'aller plus loin, je vous conseille de versionner le code source sur Github. Cela vous permet de sauvegarder ce projet, et de le mettre à jour régulièrement.
Pour ce faire, il est recommendé de vérifier la configuration .gitignore
pour être sûr de ne pas versionner les fichiers volumineux.
Je recommande la configuration suivante pour ce projet.
Vous retrouverez le code source de ce projet sur Github en cliquant sur ce lien
Merci d'avoir suivi ce tutoriel jusqu'au bout. Si vous avez des questions, n'hésitez pas à les poser en commentaire sur YouTube ou via le formulaire de contact.
Ce cours est en cours d'édition. Retrouvez la suite au format vidéo.