Développe une application de mise en relation avec Remix.js, NestJS, Typescript, Turborepo

33 minutes de lecture
Virgile RIETSCHVirgile RIETSCH

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 ?Header Icon

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 ?Header Icon

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 ?Header Icon

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 ?Header Icon

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éesHeader Icon

Présentation de la stack techniqueHeader Icon

Nous allons utiliser les technologies suivantes pour créer notre application :

FrontendHeader Icon

BackendHeader Icon

Librairies communesHeader Icon

Qualité de codeHeader Icon

DéploiementHeader Icon

Pré-requisHeader Icon

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 :

Terminal

_10
node -v

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 :

Terminal

_10
npm install -g @nestjs/cli

Configuration du projetHeader Icon

Commençons par créer un nouveau dossier qui va contenir nos deux projets.

Terminal

_10
mkdir ~/stack-remix-nestjs # Créer un dossier nommé stack-remix-nestjs à la racine de votre dossier personnel

Nous allons d'abord créer le projet NestJS, qui va se charger par la suite d'exécuter le code source de Remix.

Terminal

_10
nest new backend # Crée un projet NestJS nommé backend

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 NestJSHeader Icon

Une fois ouvert, notre interface ressemble à ceci

éditeur de code VSCode avec NestJS d'ouvert

Nous pouvons supprimer le fichier src/app.controller.spec.ts et le dossier test car nous n'en aurons pas besoin.

Création du RemixControllerHeader Icon

À 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).

src/remix/remix.controller.ts

_17
import { All, Controller, Next, Req, Res } from '@nestjs/common';
_17
import { createRequestHandler } from '@remix-run/express';
_17
import { NextFunction, Request, Response } from 'express';
_17
_17
@Controller()
_17
export class RemixController {
_17
constructor() {}
_17
_17
@All('*')
_17
async handler(
_17
@Req() request: Request,
_17
@Res() response: Response,
_17
@Next() next: NextFunction
_17
) {
_17
// Pour le moment, on ne fait rien.
_17
}
_17
}

Nous allons installer une dépendance supplémentaire pour servir l'application Remix.

Configuration de l'adapter RemixHeader Icon

Nous allons installer la librairie @remix-run/express dans notre projet NestJS.

Terminal

_10
cd backend # Pour se déplacer dans le dossier backend
_10
npm install @remix-run/express # Pour installer l'adapter Remix

Cette librairie va nous permettre de communiquer avec l'application Remix depuis NestJS.

Modifions le controlleur pour ajouter le handler de Remix.

src/remix/remix.controller.ts

_22
import { All, Controller, Next, Req, Res } from '@nestjs/common';
_22
import { createRequestHandler } from '@remix-run/express';
_22
import { NextFunction, Request, Response } from 'express';
_22
_22
@Controller()
_22
export class RemixController {
_22
constructor() {}
_22
_22
@All('*')
_22
async handler(
_22
@Req() request: Request,
_22
@Res() response: Response,
_22
@Next() next: NextFunction
_22
) {
_22
return createRequestHandler({
_22
build: '', // <= TODO: Nous devons définir le chemin du build de Remix
_22
getLoadContext: () => ({
_22
toto: 'Salut, ça va ?',
_22
}),
_22
})(request, response, next);
_22
}
_22
}

N'oublions pas d'importer ce contrôleur dans le fichier app.module.ts, pour qu'il soit détecté par NestJS.

src/app.module.ts

_10
import { Module } from '@nestjs/common';
_10
import { RemixController } from './remix/remix.controller';
_10
_10
@Module({
_10
imports: [],
_10
controllers: [RemixController],
_10
providers: [],
_10
})
_10
export class AppModule {}

Maintenant, on a besoin de générer le build de l'application Remix pour le servir depuis NestJS.

Configuration de RemixHeader Icon

Création d'un nouveau projet RemixHeader Icon

Nous devons d'abord reculer d'un dossier pour créer le projet Remix, à la racine de stack-remix-nestjs.

Terminal

_10
cd .. # Pour remonter d'un dossier
_10
npx create-remix@latest # Pour créer un nouveau projet Remix

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.

Configuration d'un nouveau projet Remix.js

Création d'un nouveau fichier index.cjs et index.d.ctsHeader Icon

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.

frontend/index.cjs

_30
const path = require('node:path');
_30
_30
let devServer;
_30
const SERVER_DIR = path.join(__dirname, 'build/server/index.js');
_30
const PUBLIC_DIR = path.join(__dirname, 'build/client');
_30
_30
module.exports.getPublicDir = function getPublicDir() {
_30
return PUBLIC_DIR;
_30
};
_30
_30
module.exports.getServerBuild = async function getServerBuild() {
_30
if (process.env.NODE_ENV === 'production' || devServer === null) {
_30
return import(SERVER_DIR);
_30
}
_30
const ssrModule = await devServer.ssrLoadModule('virtual:remix/server-build');
_30
return ssrModule;
_30
};
_30
_30
module.exports.startDevServer = async function startDevServer(app) {
_30
if (process.env.NODE_ENV === 'production') return;
_30
const vite = await import('vite');
_30
devServer = await vite.createServer({
_30
server: { middlewareMode: 'true' },
_30
root: __dirname,
_30
});
_30
_30
app.use(devServer.middlewares);
_30
return devServer;
_30
// ...continues
_30
};

Ce fichier déclare trois fonctions que nous utiliserons dans l'application NestJS :

  • getPublicDir : retourne le chemin du dossier build/client
  • getServerBuild : retourne le module server/index.js de l'application Remix
  • startDevServer : 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.

frontend/index.d.cts

_10
declare module '@virgile/frontend' {
_10
// <= Nom du module, à adapter selon le nom de votre projet
_10
export function getPublicDir(): string;
_10
export function getServerBuild(): Promise<any>;
_10
export function startDevServer(app: any): Promise<void>;
_10
}

Configurer le point d'entrée de l'application Remix dans le fichier package.jsonHeader Icon

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.

frontend/package.json

_57
{
_57
"name": "@virgile/frontend",
_57
"private": true,
_57
"sideEffects": false,
_57
"type": "module",
_57
"main": "./index.cjs",
_57
"types": "./index.d.cts",
_57
"scripts": {
_57
"start": "remix-serve ./build/server/index.js",
_57
"old-dev": "remix vite:dev",
_57
"build": "remix vite:build",
_57
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
_57
"typecheck": "tsc"
_57
},
_57
"dependencies": {
_57
"@conform-to/react": "^1.1.0",
_57
"@conform-to/zod": "^1.1.0",
_57
"@radix-ui/react-slot": "^1.0.2",
_57
"@remix-run/node": "^2.8.1",
_57
"@remix-run/react": "^2.8.1",
_57
"@remix-run/serve": "^2.8.1",
_57
"class-variance-authority": "^0.7.0",
_57
"clsx": "^2.1.0",
_57
"isbot": "^5.1.2",
_57
"lucide-react": "^0.365.0",
_57
"react": "^18.2.0",
_57
"react-dom": "^18.2.0",
_57
"remix-flat-routes": "^0.6.4",
_57
"tailwind-merge": "^2.2.2",
_57
"tailwindcss-animate": "^1.0.7",
_57
"zod": "^3.22.4"
_57
},
_57
"devDependencies": {
_57
"@remix-run/dev": "^2.8.1",
_57
"@types/react": "^18.2.67",
_57
"@types/react-dom": "^18.2.22",
_57
"@virgile/eslint-config": "*",
_57
"@virgile/typescript-config": "*",
_57
"autoprefixer": "^10.4.19",
_57
"eslint": "^8.57.0",
_57
"eslint-import-resolver-typescript": "^3.6.1",
_57
"eslint-plugin-import": "^2.29.1",
_57
"eslint-plugin-jsx-a11y": "^6.8.0",
_57
"eslint-plugin-react": "^7.34.1",
_57
"eslint-plugin-react-hooks": "^4.6.0",
_57
"eslint-plugin-remix-react-routes": "^1.0.5",
_57
"eslint-plugin-tailwindcss": "^3.15.1",
_57
"postcss": "^8.4.38",
_57
"tailwindcss": "^3.4.3",
_57
"typescript": "^5.4.3",
_57
"vite": "^5.2.2",
_57
"vite-tsconfig-paths": "^4.3.2"
_57
},
_57
"engines": {
_57
"node": ">=18.0.0"
_57
}
_57
}

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 monorepoHeader Icon

Pour configurer le monorepo, nous avons besoin de reculer d'un dossier.

Terminal

_10
cd .. # Pour remonter d'un dossier, si vous êtes dans le dossier frontend

À l'intérieur du dossier stack-remix-nestjs, nous allons instancier un nouveau fichier package.json pour configurer notre monorepo.

Terminal

_10
npm init -y # Pour créer un fichier package.json

Le fichier package.json doit ressembler à ça :

package.json

_13
{
_13
"name": "nestjs-remix-monorepo",
_13
"version": "1.0.0",
_13
"description": "",
_13
"main": "index.js",
_13
"scripts": {
_13
"test": "echo \"Error: no test specified\" && exit 1"
_13
},
_13
"keywords": [],
_13
"author": "",
_13
"license": "ISC",
_13
"packageManager": "npm@10.2.3"
_13
}

Installation de TurboHeader Icon

Nous allons utiliser une librairie appelée Turborepo pour gérer notre monorepo.

Terminal

_10
npm install -D turbo

Nous pouvons aussi l'installer en global pour bénéficier de la commande turbo dans le terminal.

Terminal

_10
npm install -g turbo

Ensuite, nous allons déclarer nos deux projets comme des workspaces dans le fichier package.json.

Le fichier devrait maintenant ressembler à ça :

package.json

_17
{
_17
"name": "nestjs-remix-monorepo",
_17
"version": "1.0.0",
_17
"description": "",
_17
"main": "index.js",
_17
"scripts": {
_17
"test": "echo \"Error: no test specified\" && exit 1"
_17
},
_17
"keywords": [],
_17
"author": "",
_17
"license": "ISC",
_17
"packageManager": "npm@10.2.3",
_17
"workspaces": ["frontend", "backend"],
_17
"devDependencies": {
_17
"turbo": "^1.13.0"
_17
}
_17
}

Création du fichier turbo.jsonHeader Icon

Nous devons ensuite créer un fichier de configuration Turbo. Pour ce faire, nous pouvons créer un fichier nommé turbo.json.

Terminal

_10
touch turbo.json # Pour créer un fichier turbo.json

Ensuite, nous allons coller la configuration par défaut, qu'on retrouve sur la documentation officielle :

turbo.json

_10
{
_10
"$schema": "https://turbo.build/schema.json",
_10
"pipeline": {
_10
"build": {
_10
"outputs": ["dist/**"]
_10
},
_10
"type-check": {}
_10
}
_10
}

(Optionnel) Création d'un fichier .gitignoreHeader Icon

Nous pouvons ajouter un fichier .gitignore pour éviter de versionner les fichiers volumineux.

Terminal

_10
touch .gitignore # Pour créer un fichier .gitignore


_10
.turbo
_10
node_modules
_10
/dist
_10
/build
_10
/out

Création de pipelines avec TurborepoHeader Icon

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.

backend/package.json

_10
"scripts": {
_10
"dev": "run-p dev:compile dev:watch",
_10
"dev:compile": "tsc --build --watch",
_10
"dev:watch": "nodemon node dist/main.js",
_10
}

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.

Terminal

_10
cd backend # Pour se déplacer dans le dossier backend
_10
npm install -D nodemon npm-run-all # Pour installer les dépendances

Ensuite, renommons la commande devpar old-dev dans le frontend/package.json pour l'empêcher de s'exécuter.

frontend/package.json

_10
"scripts": {
_10
"old-dev": "remix vite:dev"
_10
}

Nous devons maintenant créer une pipeline dev dans notre fichier de configuration turbo.json

turbo.json

_12
{
_12
"$schema": "https://turbo.build/schema.json",
_12
"pipeline": {
_12
"build": {
_12
"outputs": ["dist/**"]
_12
},
_12
"dev": {
_12
"cache": false,
_12
"persistent": true
_12
}
_12
}
_12
}

Pour exécuter cette pipeline, nous devons lancer la commande turbo dans le terminal, suivi du nom de la pipeline.

Terminal

_10
turbo dev

Nous allons créer une commande dans le fichier package.json de la racine pour exécuter cette pipeline.

package.json

_10
"scripts": {
_10
"dev": "turbo dev"
_10
}

Renommer la librairie frontend pour l'importer dans le backendHeader Icon

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.

Erreur lors du lancement de notre application NestJS

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.

backend/package.json

_10
"name": "@virgile/backend",

frontend/package.json

_10
"name": "@virgile/frontend",

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.

backend/package.json

_10
"dependencies": {
_10
"@virgile/frontend": "*" // Nous informons le monorepo qu'il doit utiliser notre librairie frontend dans le projet backend
_10
}

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

frontend/index.d.cts

_10
declare module '@virgile/backend' {
_10
// <= Nom du module, à adapter selon le nom de votre projet
_10
export function getPublicDir(): string;
_10
export function getServerBuild(): Promise<any>;
_10
export function startDevServer(app: any): Promise<void>;
_10
}

Importer getServerBuild dans RemixControllerHeader Icon

Nous pouvons à présent importer la méthode getServerBuild dans le fichier backend/src/remix/remix.controller.ts.

Voici le fichier modifié :

backend/src/remix/remix.controller.ts

_22
import { All, Controller, Next, Req, Res } from '@nestjs/common';
_22
import { createRequestHandler } from '@remix-run/express';
_22
import { getServerBuild } from '@virgile/frontend';
_22
import { NextFunction, Request, Response } from 'express';
_22
_22
@Controller()
_22
export class RemixController {
_22
@All('*')
_22
async handler(
_22
@Req() request: Request,
_22
@Res() response: Response,
_22
@Next() next: NextFunction
_22
) {
_22
//
_22
return createRequestHandler({
_22
build: await getServerBuild(),
_22
getLoadContext: () => ({
_22
toto: 'Salut, ça va ?',
_22
}),
_22
})(request, response, next);
_22
}
_22
}

Utiliser les méthodes getPublicDir et startDevServerHeader Icon

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.

backend/src/main.ts

_24
import { NestFactory } from '@nestjs/core';
_24
import { NestExpressApplication } from '@nestjs/platform-express';
_24
import { getPublicDir, startDevServer } from '@virgile/frontend';
_24
import { AppModule } from './app.module';
_24
_24
async function bootstrap() {
_24
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
_24
bodyParser: false,
_24
});
_24
_24
await startDevServer(app);
_24
_24
app.useStaticAssets(getPublicDir(), {
_24
immutable: true,
_24
maxAge: '1y',
_24
index: false,
_24
});
_24
_24
const selectedPort = process.env.PORT ?? 3000;
_24
_24
console.log(`Running on port http://localhost:${selectedPort}`);
_24
await app.listen(selectedPort);
_24
}
_24
bootstrap();

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éveloppementHeader Icon

Nous pouvons à présent lancer le serveur de développement en exécutant la commande npm run dev dans le terminal.

Terminal

_10
npm run dev

Le serveur devrait démarrer sans erreur.

Notre serveur NestJS tourne sans erreur.

Ajouter Typescript et EslintHeader Icon

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.

Terminal

_10
mkdir packages # Pour créer un dossier nommé packages

Il va contenir nos configurations Typescript et Eslint.

Configurer TypescriptHeader Icon

Pour ajouter notre configuration Typescript, nous allons créer un nouveau dossier typescript-config à l'intérieur de packages.

Terminal

_10
mkdir packages/typescript-config # Pour créer un dossier nommé typescript-config

Nous allons ensuite générer un fichier package.json, pour télécharger les dépendances nécessaires.

Terminal

_10
cd packages/typescript-config # Pour se déplacer dans le dossier typescript-config
_10
npm init -y # Pour créer un fichier package.json

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.

packages/typescript-config/base.json

_19
{
_19
"compilerOptions": {
_19
"module": "CommonJS",
_19
"declaration": true,
_19
"removeComments": true,
_19
"allowSyntheticDefaultImports": true,
_19
"target": "ES2022",
_19
"sourceMap": true,
_19
"strict": true,
_19
"skipLibCheck": true,
_19
"forceConsistentCasingInFileNames": true,
_19
"esModuleInterop": true,
_19
"isolatedModules": false,
_19
"moduleResolution": "Node",
_19
"resolveJsonModule": false,
_19
"allowJs": false,
_19
"lib": ["ES2023"]
_19
}
_19
}

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.

backend/package.json

_10
"devDependencies": {
_10
"@virgile/typescript-config": "*"
_10
}

frontend/package.json

_10
"devDependencies": {
_10
"@virgile/typescript-config": "*"
_10
}

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.

backend/tsconfig.json

_13
{
_13
"extends": "@virgile/typescript-config/base.json",
_13
"compilerOptions": {
_13
"emitDecoratorMetadata": true,
_13
"experimentalDecorators": true,
_13
"strict": true,
_13
"outDir": "./dist",
_13
"rootDir": "./src",
_13
"noEmit": false,
_13
"lib": ["DOM", "ES2023"]
_13
},
_13
"include": ["src/**/*.ts", "src/exports.js"]
_13
}

frontend/tsconfig.json

_31
{
_31
"extends": "@virgile/typescript-config/base.json",
_31
"include": [
_31
"env.d.ts",
_31
"**/*.ts",
_31
"**/*.tsx",
_31
"../playwright.config.ts",
_31
"tailwind.config.cjs"
_31
],
_31
"exclude": ["index.d.cts"],
_31
"compilerOptions": {
_31
"module": "ESNext",
_31
"skipLibCheck": true,
_31
"lib": ["DOM", "DOM.Iterable", "ES2019"],
_31
"isolatedModules": true,
_31
"esModuleInterop": false,
_31
"jsx": "react-jsx",
_31
"noImplicitAny": false,
_31
"moduleResolution": "bundler",
_31
"resolveJsonModule": true,
_31
"target": "ES2019",
_31
"strict": true,
_31
"allowJs": true,
_31
"forceConsistentCasingInFileNames": true,
_31
"baseUrl": ".",
_31
"paths": {
_31
"~/*": ["./app/*"]
_31
},
_31
"noEmit": true
_31
}
_31
}

Configurer EslintHeader Icon

Nous allons reproduire la même opération pour Eslint.

Créons un nouveau dossier packages/eslint-config à la racine de notre monorepo.

Terminal

_10
mkdir packages/eslint-config # Pour créer un dossier nommé eslint-config

Nous allons ensuite générer un fichier package.json, pour télécharger les dépendances nécessaires.

Terminal

_10
cd packages/eslint-config # Pour se déplacer dans le dossier eslint-config
_10
npm init -y # Pour créer un fichier package.json

Nous devons ensuite installer toutes les librairies relatives à nos règles Eslint.

packages/eslint-config/package.Jso

_22
{
_22
"name": "@virgile/eslint-config",
_22
"version": "0.0.0",
_22
"private": true,
_22
"license": "MIT",
_22
"files": ["base.js"],
_22
"devDependencies": {
_22
"@remix-run/eslint-config": "^2.8.1",
_22
"@types/eslint": "^8.56.5",
_22
"@typescript-eslint/eslint-plugin": "^7.2.0",
_22
"@typescript-eslint/parser": "^7.2.0",
_22
"eslint": "^8.57.0",
_22
"eslint-config-prettier": "^9.1.0",
_22
"eslint-plugin-import": "^2.29.1",
_22
"eslint-plugin-prettier": "^5.1.3",
_22
"typescript": "^5.4.2"
_22
},
_22
"prettier": {},
_22
"scripts": {
_22
"lint:parent": "cd ../../ && npm run lint"
_22
}
_22
}

Et nous allons également copier sa configuration globale.

packages/eslint-config/base.js

_28
/** @type {import('@types/eslint').Linter.BaseConfig} */
_28
module.exports = {
_28
extends: [
_28
"@remix-run/eslint-config",
_28
"@remix-run/eslint-config/node",
_28
"prettier",
_28
"plugin:import/recommended",
_28
],
_28
parser: "@typescript-eslint/parser",
_28
rules: {
_28
"import/no-duplicates": ["warn", { "prefer-inline": true }],
_28
"import/consistent-type-specifier-style": ["warn", "prefer-inline"],
_28
"import/order": [
_28
"warn",
_28
{
_28
alphabetize: { order: "asc", caseInsensitive: true },
_28
groups: [
_28
"builtin",
_28
"external",
_28
"internal",
_28
"parent",
_28
"sibling",
_28
"index",
_28
],
_28
},
_28
],
_28
},
_28
};

Maintenant, ajoutons cette librairie comme dépendance de nos projets.

backend/package.json

_10
"devDependencies": {
_10
"@virgile/eslint-config": "*"
_10
}

frontend/package.json

_10
"devDependencies": {
_10
"@virgile/eslint-config": "*"
_10
}

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.

backend/.eslintrc.js

_23
module.exports = {
_23
parser: '@typescript-eslint/parser',
_23
parserOptions: {
_23
project: 'tsconfig.json',
_23
tsconfigRootDir: __dirname,
_23
sourceType: 'module',
_23
},
_23
extends: [
_23
'plugin:@typescript-eslint/recommended'
_23
],
_23
root: true,
_23
env: {
_23
node: true,
_23
jest: true
_23
},
_23
ignorePatterns: ['.eslintrc.js', 'dist/', 'node_modules/', '**.d.ts'],
_23
rules: {
_23
'@typescript-eslint/interface-name-prefix': 'off',
_23
'@typescript-eslint/explicit-function-return-type': 'off',
_23
'@typescript-eslint/explicit-module-boundary-types': 'off',
_23
'@typescript-eslint/no-explicit-any': 'off'
_23
}
_23
};

Nous l'utilisons essentiellement pour notre application Remix !

frontend/.eslintrc.cjs

_36
/** @type {import("eslint").Linter.Config} */
_36
module.exports = {
_36
root: true,
_36
extends: [
_36
'@virgile/eslint-config/base.js',
_36
'@remix-run/eslint-config',
_36
'@remix-run/eslint-config/node',
_36
'plugin:remix-react-routes/recommended',
_36
],
_36
settings: {
_36
'import/resolver': {
_36
node: {
_36
extensions: ['.js', '.jsx', '.ts', '.tsx'],
_36
},
_36
},
_36
},
_36
_36
overrides: [
_36
{
_36
extends: ['@remix-run/eslint-config/jest-testing-library'],
_36
files: ['app/**/__tests__/**/*', 'app/**/*.{spec,test}.*'],
_36
rules: {
_36
'testing-library/no-await-sync-events': 'off',
_36
'jest-dom/prefer-in-document': 'off',
_36
},
_36
// we're using vitest which has a very similar API to jest
_36
// (so the linting plugins work nicely), but it means we have to explicitly
_36
// set the jest version.
_36
settings: {
_36
jest: {
_36
version: 28,
_36
},
_36
},
_36
},
_36
],
_36
};

Ajouter ces packages en tant que workspacesHeader Icon

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.

package.json

_15
{
_15
"name": "nestjs-remix-monorepo",
_15
"version": "1.0.0",
_15
"description": "",
_15
"main": "index.js",
_15
"scripts": {
_15
"test": "echo \"Error: no test specified\" && exit 1",
_15
"dev": "turbo dev"
_15
},
_15
"keywords": [],
_15
"author": "",
_15
"license": "ISC",
_15
"packageManager": "npm@10.2.3",
_15
"workspaces": ["frontend", "backend", "packages/*"]
_15
}

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émonstrationHeader Icon

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 RemixHeader Icon

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).

frontend/app/routes/api.tsx

_10
import { json } from '@remix-run/node';
_10
export const loader = () => {
_10
return json({
_10
status: 'ok',
_10
});
_10
};

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.

localhost:3000/api

_10
{
_10
"status": "ok"
_10
}

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.

frontend/app/routes/api.tsx

_10
import { json } from '@remix-run/node';
_10
export const loader = () => {
_10
return json({
_10
status: 'ok',
_10
});
_10
};
_10
_10
export default function Page() {
_10
return <h1>Hello</h1>;
_10
}

Actualisons la page. Nous devrions voir le texte Hello s'afficher.

Notre page Remix affiche un composant Hello

Deuxième victoire ! Notre route Remix fonctionne correctement. Nous allons maintenant importer un service NestJS dans notre application Remix.

Ajout du RemixService (dans NestJS)Header Icon

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.

backend/src/remix/remix.service.ts

_10
import { Injectable } from '@nestjs/common';
_10
_10
@Injectable()
_10
export class RemixService {
_10
public readonly getHello = (): string => {
_10
return 'Hello World!';
_10
};
_10
}

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

backend/src/app.module.ts

_10
import { Module } from '@nestjs/common';
_10
import { RemixController } from './remix/remix.controller';
_10
import { RemixService } from './remix/remix.service';
_10
_10
@Module({
_10
imports: [],
_10
controllers: [RemixController],
_10
providers: [RemixService],
_10
})
_10
export class AppModule {}

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.

backend/package.json

_10
{
_10
"main": "./dist/remix/remix.service.js",
_10
"types": "./dist/remix/remix.service.d.ts"
_10
}

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.

Terminal

_10
npm run build

Comment utiliser ce service ? Nous allons l'importer dans notre controlleur RemixController.

Nous allons ensuite le renvoyer au contexte de Remix, tel quel.

backend/src/remix/remix.controller.ts

_26
import { All, Controller, Next, Req, Res } from '@nestjs/common';
_26
import { createRequestHandler } from '@remix-run/express';
_26
import { getServerBuild } from '@virgile/frontend';
_26
import { NextFunction, Request, Response } from 'express';
_26
import { RemixService } from './remix.service';
_26
_26
@Controller()
_26
export class RemixController {
_26
constructor(private remixService: RemixService) {}
_26
_26
@All('*')
_26
async handler(
_26
@Req() request: Request,
_26
@Res() response: Response,
_26
@Next() next: NextFunction
_26
) {
_26
//
_26
return createRequestHandler({
_26
build: await getServerBuild(),
_26
getLoadContext: () => ({
_26
toto: 'Salut, ça va ?',
_26
remixService: this.remixService,
_26
}),
_26
})(request, response, next);
_26
}
_26
}

N'oublions pas de re-compiler ce code avec la commande npm run dev, avant de passer à la suite.

De retour dans Remix.jsHeader Icon

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.

frontend/app/routes/api.tsx

_10
import { json, type LoaderFunctionArgs } from '@remix-run/node';
_10
export const loader = ({ context }: LoaderFunctionArgs) => {
_10
return json({
_10
status: 'ok',
_10
});
_10
};
_10
_10
export default function Page() {
_10
return <h1>Hello</h1>;
_10
}

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 RemixHeader Icon

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

frontend/root.tsx

_10
declare module '@remix-run/node' {
_10
interface AppLoadContext {
_10
remixService: RemixService;
_10
toto: string;
_10
}
_10
}

Bien sûr, nous avons également besoin d'importer le type RemixService, en haut du fichier.

frontend/root.tsx

_10
import { type RemixService } from '@virgile/backend';

Cela nous donne le fichier suivant :

frontend/root.tsx

_38
import type { LoaderFunctionArgs } from '@remix-run/node';
_38
import {
_38
Links,
_38
Meta,
_38
Outlet,
_38
Scripts,
_38
ScrollRestoration,
_38
} from '@remix-run/react';
_38
import { type RemixService } from '@virgile/backend';
_38
_38
declare module '@remix-run/node' {
_38
interface AppLoadContext {
_38
remixService: RemixService;
_38
user: unknown;
_38
}
_38
}
_38
_38
export function Layout({ children }: { children: React.ReactNode }) {
_38
return (
_38
<html lang='en' className='h-full'>
_38
<head>
_38
<meta charSet='utf-8' />
_38
<meta name='viewport' content='width=device-width, initial-scale=1' />
_38
<Meta />
_38
<Links />
_38
</head>
_38
<body className='min-h-screen flex flex-col'>
_38
{children}
_38
<ScrollRestoration />
_38
<Scripts />
_38
</body>
_38
</html>
_38
);
_38
}
_38
_38
export default function App() {
_38
return <Outlet />;
_38
}

Grâce à cet ajout, nous possédons maintenant l'autocomplétion dans le context de Remix. Regardez:

Notre context Remix.js possède maintenant l'autocomplétion depuis l'application NestJS

Utiliser le service RemixService dans notre routeHeader Icon

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
frontend/app/routes/api.tsx

_11
import { json, type LoaderFunctionArgs } from '@remix-run/node';
_11
export const loader = ({ context }: LoaderFunctionArgs) => {
_11
return json({
_11
message: context.remixService.getHello(),
_11
});
_11
};
_11
_11
export default function Page() {
_11
const { message } = useLoaderData<typeof loader>();
_11
return <h1>{message}</h1>;
_11
}

Notre route Remix réussit à appeler une méthode définie dans notre application NestJS

Il nous reste plus qu'à configurer Turborepo ! Ensuite, nous pourrons passer au développement de notre application.

Configuration de TurborepoHeader Icon

Ajout des commandes typecheck et lintHeader Icon

Nous avons déjà ajouté le mot clé workspaces dans le package.json principal. Il informe NodeJS que nous utilisons un monorepo.

package.json

_10
{
_10
"workspaces": ["frontend", "backend", "packages/*"]
_10
}

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

package.json

_15
{
_15
"name": "nestjs-remix-monorepo",
_15
"version": "1.0.0",
_15
"description": "",
_15
"main": "index.js",
_15
"scripts": {
_15
"test": "echo \"Error: no test specified\" && exit 1",
_15
"dev": "turbo dev"
_15
},
_15
"keywords": [],
_15
"author": "",
_15
"license": "ISC",
_15
"packageManager": "npm@10.2.3",
_15
"workspaces": ["frontend", "backend", "packages/*"]
_15
}

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

turbo.json

_12
{
_12
"$schema": "https://turbo.build/schema.json",
_12
"pipeline": {
_12
"build": {
_12
"outputs": ["dist/**"]
_12
},
_12
"dev": {
_12
"cache": false,
_12
"persistent": true
_12
}
_12
}
_12
}

Nous allons rajouter deux autres pipelines à cette configuration, pour utiliser Eslint et Typescript.

turbo.json

_14
{
_14
"$schema": "https://turbo.build/schema.json",
_14
"pipeline": {
_14
"build": {
_14
"outputs": ["dist/**"]
_14
},
_14
"dev": {
_14
"cache": false,
_14
"persistent": true
_14
},
_14
"typecheck": {},
_14
"lint": {}
_14
}
_14
}

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 :

package.json

_18
{
_18
"name": "nestjs-remix-monorepo",
_18
"version": "1.0.0",
_18
"description": "",
_18
"main": "index.js",
_18
"scripts": {
_18
"test": "echo \"Error: no test specified\" && exit 1",
_18
"build": "turbo build",
_18
"dev": "turbo dev",
_18
"typecheck": "turbo typecheck",
_18
"lint": "turbo lint"
_18
},
_18
"keywords": [],
_18
"author": "",
_18
"license": "ISC",
_18
"packageManager": "npm@10.2.3",
_18
"workspaces": ["frontend", "backend", "packages/*"]
_18
}

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.

frontend/package.json

_45
{
_45
"name": "@virgile/frontend",
_45
"private": true,
_45
"sideEffects": false,
_45
"type": "module",
_45
"main": "./index.cjs",
_45
"types": "./index.d.cts",
_45
"scripts": {
_45
"start": "remix-serve ./build/server/index.js",
_45
"old-dev": "remix vite:dev",
_45
"build": "remix vite:build",
_45
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
_45
"typecheck": "tsc"
_45
},
_45
"dependencies": {
_45
"@remix-run/node": "^2.8.1",
_45
"@remix-run/react": "^2.8.1",
_45
"@remix-run/serve": "^2.8.1",
_45
"isbot": "^5.1.2",
_45
"react": "^18.2.0",
_45
"react-dom": "^18.2.0",
_45
"remix-flat-routes": "^0.6.4"
_45
},
_45
"devDependencies": {
_45
"@remix-run/dev": "^2.8.1",
_45
"@types/react": "^18.2.67",
_45
"@types/react-dom": "^18.2.22",
_45
"@virgile/eslint-config": "*",
_45
"@virgile/typescript-config": "*",
_45
"eslint": "^8.57.0",
_45
"eslint-import-resolver-typescript": "^3.6.1",
_45
"eslint-plugin-import": "^2.29.1",
_45
"eslint-plugin-jsx-a11y": "^6.8.0",
_45
"eslint-plugin-react": "^7.34.1",
_45
"eslint-plugin-react-hooks": "^4.6.0",
_45
"eslint-plugin-remix-react-routes": "^1.0.5",
_45
"eslint-plugin-tailwindcss": "^3.15.1",
_45
"typescript": "^5.4.3",
_45
"vite": "^5.2.2",
_45
"vite-tsconfig-paths": "^4.3.2"
_45
},
_45
"engines": {
_45
"node": ">=18.0.0"
_45
}
_45
}

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 buildHeader Icon

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).

turbo.json

_14
{
_14
"$schema": "https://turbo.build/schema.json",
_14
"pipeline": {
_14
"build": {
_14
"outputs": ["dist/**"] // <= Cette ligne est erronée
_14
},
_14
"dev": {
_14
"cache": false,
_14
"persistent": true
_14
},
_14
"typecheck": {},
_14
"lint": {}
_14
}
_14
}

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.

package.json

_10
{
_10
"dependencies": {
_10
"@virgile/frontend": "*"
_10
}
_10
}

Ajoutons la clé "dependsOn": ["^build"] au fichier de configuration pour que Turbo en soit informé.

turbo.json

_15
{
_15
"$schema": "https://turbo.build/schema.json",
_15
"pipeline": {
_15
"build": {
_15
"outputs": ["backend/dist/**", "frontend/build/**"],
_15
"dependsOn": ["^build"]
_15
},
_15
"dev": {
_15
"cache": false,
_15
"persistent": true
_15
},
_15
"typecheck": {},
_15
"lint": {}
_15
}
_15
}

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

Le build de notre application échoue

Modification de la configuration Vite de RemixHeader Icon

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 :

frontend/vite.config.ts

_59
import { vitePlugin as remix } from '@remix-run/dev';
_59
import { installGlobals } from '@remix-run/node';
_59
import { resolve } from 'path';
_59
import { flatRoutes } from 'remix-flat-routes';
_59
import { defineConfig } from 'vite';
_59
import tsconfigPaths from 'vite-tsconfig-paths';
_59
_59
const MODE = process.env.NODE_ENV;
_59
installGlobals();
_59
_59
export default defineConfig({
_59
resolve: {
_59
preserveSymlinks: true,
_59
},
_59
build: {
_59
cssMinify: MODE === 'production',
_59
sourcemap: true,
_59
commonjsOptions: {
_59
include: [/frontend/, /backend/, /node_modules/],
_59
},
_59
},
_59
plugins: [
_59
// cjsInterop({
_59
// dependencies: ['remix-utils', 'is-ip', '@markdoc/markdoc'],
_59
// }),
_59
tsconfigPaths({}),
_59
remix({
_59
ignoredRouteFiles: ['**/*'],
_59
future: {
_59
v3_fetcherPersist: true,
_59
},
_59
_59
// When running locally in development mode, we use the built in remix
_59
// server. This does not understand the vercel lambda module format,
_59
// so we default back to the standard build output.
_59
// ignoredRouteFiles: ['**/.*', '**/*.test.{js,jsx,ts,tsx}'],
_59
serverModuleFormat: 'esm',
_59
_59
routes: async (defineRoutes) => {
_59
return flatRoutes('routes', defineRoutes, {
_59
ignoredRouteFiles: [
_59
'.*',
_59
'**/*.css',
_59
'**/*.test.{js,jsx,ts,tsx}',
_59
'**/__*.*',
_59
// This is for server-side utilities you want to colocate next to
_59
// your routes without making an additional directory.
_59
// If you need a route that includes "server" or "client" in the
_59
// filename, use the escape brackets like: my-route.[server].tsx
_59
// '**/*.server.*',
_59
// '**/*.client.*',
_59
],
_59
// Since process.cwd() is the server directory, we need to resolve the path to remix project
_59
appDir: resolve(__dirname, 'app'),
_59
});
_59
},
_59
}),
_59
],
_59
});

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.

Terminal

_10
npm run build

Le build de notre application a réussi

Compilation de l'application NestJS avec TypescriptHeader Icon

Nous avons presque terminé cette configuration. Cependant, une petite modification du package.json de notre application NestJS s'impose.

backend/package.json

_48
{
_48
"name": "@virgile/backend",
_48
"version": "0.0.1",
_48
"description": "",
_48
"author": "",
_48
"private": true,
_48
"license": "UNLICENSED",
_48
"main": "./dist/remix/remix.service.js",
_48
"types": "./dist/remix/remix.service.d.ts",
_48
"scripts": {
_48
"dev": "run-p dev:compile dev:watch",
_48
"dev:compile": "tsc --build --watch",
_48
"dev:watch": "nodemon node dist/main.js",
_48
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
_48
"prebuild": "rimraf dist tsconfig.tsbuildinfo",
_48
"build": "tsc --build",
_48
"start": "node dist/main",
_48
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
_48
"typecheck": "tsc --noEmit"
_48
},
_48
"dependencies": {
_48
"@nestjs/common": "^10.0.0",
_48
"@nestjs/core": "^10.0.0",
_48
"@nestjs/platform-express": "^10.0.0",
_48
"@remix-run/express": "^2.8.1",
_48
"reflect-metadata": "^0.2.0",
_48
"rxjs": "^7.8.1"
_48
},
_48
"devDependencies": {
_48
"@nestjs/cli": "^10.0.0",
_48
"@nestjs/schematics": "^10.0.0",
_48
"@nestjs/testing": "^10.0.0",
_48
"@types/express": "^4.17.17",
_48
"@types/jest": "^29.5.2",
_48
"@types/node": "^20.3.1",
_48
"@virgile/eslint-config": "*",
_48
"@virgile/frontend": "*",
_48
"@virgile/typescript-config": "*",
_48
"nodemon": "^3.1.0",
_48
"npm-run-all": "^4.1.5",
_48
"prettier": "^3.0.0",
_48
"source-map-support": "^0.5.21",
_48
"ts-loader": "^9.4.3",
_48
"ts-node": "^10.9.1",
_48
"tsconfig-paths": "^4.2.0",
_48
"typescript": "^5.1.3"
_48
}
_48
}

Qu'avons-nous changé ?

  • La commande dev exécute deux commandes en parallèle (grâce à la librairie npm-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 commande prebuild se lance automatiquement lorsque l'on exécute la commande build
  • 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 applicationHeader Icon

Avant de versionner notre code sur Github, testons chacune des commandes une par une.

Erreurs de typeHeader Icon

Détectons les erreurs de type. Normalement, il ne devrait pas en détecter.

Terminal

_10
npm run typecheck

Bonnes pratiquesHeader Icon

Détectons les bonnes pratiques avec Eslint. Il se peut qu'il nous signale quelques erreurs.

Terminal

_10
npm run lint

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.

Terminal

_10
touch .eslintignore

.eslintignore

_10
build
_10
node_modules

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.

turbo.json

_19
{
_19
"$schema": "https://turbo.build/schema.json",
_19
"pipeline": {
_19
"build": {
_19
"outputs": ["backend/dist/**", "frontend/build/**"],
_19
"dependsOn": ["^build"]
_19
},
_19
"dev": {
_19
"cache": false,
_19
"persistent": true
_19
},
_19
"typecheck": {
_19
"cache": false
_19
},
_19
"lint": {
_19
"cache": false
_19
}
_19
}
_19
}

Relançons la commance pour bénéficier d'une analyse fiable, sans cache.

Terminal

_10
npm run lint

Build de l'applicationHeader Icon

Compilons le code source de l'application. Normalement, nous avons résolu toutes les erreurs.

Terminal

_10
npm run build

Lancement de l'applicationHeader Icon

Nous avons oublié d'ajouter la commande start au fichier package.json de notre application. Ajoutons-là tout de suite.

package.json

_10
{
_10
"scripts": {
_10
"start": "cd backend && npm run start"
_10
}
_10
}

Cette commande va exécuter la commande start de notre backend/package.json

On peut maintenant la lancer à la racine du projet.

Terminal

_10
npm start

Notre application Remix + NestJS s'exécute avec succès !

Héberger le code source sur GithubHeader Icon

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.

Terminal

_10
touch .gitignore

Je recommande la configuration suivante pour ce projet.

.gitignore

_50
.turbo
_50
node_modules
_50
_50
/dist
_50
/node_modules
_50
/build
_50
_50
logs
_50
*.log
_50
npm-debug.log*
_50
pnpm-debug.log*
_50
yarn-debug.log*
_50
yarn-error.log*
_50
lerna-debug.log*
_50
_50
.DS_Store
_50
_50
/coverage
_50
/.nyc_output
_50
_50
/.idea
_50
.project
_50
.classpath
_50
.c9/
_50
*.launch
_50
.settings/
_50
*.sublime-workspace
_50
_50
.vscode/*
_50
!.vscode/settings.json
_50
!.vscode/tasks.json
_50
!.vscode/launch.json
_50
!.vscode/extensions.json
_50
_50
.env
_50
.env.development.local
_50
.env.test.local
_50
.env.production.local
_50
.env.local
_50
_50
.temp
_50
.tmp
_50
_50
pids
_50
*.pid
_50
*.seed
_50
*.pid.lock
_50
_50
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
_50
out/

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.

Déploiement de l'applicationHeader Icon

Intégration de TailwindCSSHeader Icon

Authentification avec RedisHeader Icon

Partie 1Header Icon

Partie 2Header Icon