---
title: "Ajouter un frontend React 19 à un module Nodefony"
source: docs/guides/frontend-react.md
url: https://nodefony.github.io/nodefony-core/docs/guides/frontend-react/
---


> Tu as un module Nodefony existant et tu veux y ajouter une SPA React 19 servie par Vite, avec HMR en dev et build optimisé en prod. Ce guide te montre comment.

**Audience** : développeur Nodefony qui connaît déjà `Module`, `Service`, `Controller`. Si non, lis d'abord [`../architecture/injection-portees.md`](../../architecture/injection-portees/) puis reviens.

**Résultat final** : une page React montée à `https://localhost:5152/shop-front/` qui appelle ton API backend (`/shop-front/api/data`) via le proxy Vite, avec HMR fonctionnel.

📍 [Documentation](../../) › [Guides](../) › **Frontend React**

## Le modèle — deux serveurs, un seul site

Pendant le développement, **deux** serveurs tournent : Nodefony sert votre application, Vite sert
les modules du frontend et pousse le rechargement à chaud. Le visiteur, lui, n'en voit qu'un seul :
Nodefony rend la page, y insère les balises du frontend (`FrontendService.renderTags()`,
`FrontendService.ts#FrontendService.renderTags`) — servies sur l'origine de la page et relayées vers Vite
(`FrontendService.mountDevProxy()`, `FrontendService.ts#FrontendService.mountDevProxy`). Les appels d'API de la page
partent sur cette même origine, donc directement vers tes contrôleurs. En production il n'y a plus qu'un serveur : les fichiers
sont bâtis, et les mêmes balises pointent vers eux.

Un module déclare son frontend une seule fois, par `FrontendService.registerEntry()`
(`FrontendService.ts#FrontendService.registerEntry`) — c'est ce point d'entrée qui fait exister le tout.

## Approche express — la commande le fait

```bash
nodefony create module shop --frontend react   # ou : vue | angular
```

La commande produit le squelette du module **et** son frontend : controller HTML avec sa politique
de sécurité, dossier `frontend/`, dépendances déclarées, point d'entrée enregistré. Les trois
choix sont **React 19, Vue 3 et Angular** (standalone, zoneless) — elle vous fait gagner les neuf
étapes manuelles ci-dessous. Svelte 5 existe aussi, mais par `nodefony create front`, qui pose une
page dans une application ou un module déjà créé.

Le reste de ce guide explique la **version manuelle**, utile pour comprendre ce que la commande
fait, et indispensable pour greffer un frontend sur un module qui existe déjà.

---

## Pré-requis

- `@nodefony/frontend` installé et déclaré dans l'application (étape 1 ci-dessous)
- Vite et ses plugins installés à la racine :
  ```bash
  npm i -D vite @vitejs/plugin-react react react-dom
  ```
- Connaissance basique React 19 + JSX

## Étape 1 — Activer `@nodefony/frontend` dans l'app

Dans le manifeste `modules` de `nodefony.config.ts` — son ordre est l'ordre de chargement :

```typescript
// nodefony.config.ts
import { defineConfig } from "nodefony";

export default defineConfig({
  modules: [
    "@nodefony/http",
    "@nodefony/framework",
    "@nodefony/frontend", // ← AVANT ton module consumer (ordre de chargement)
    "shop-front", // ← ton module à venir
  ],
});
```

> ⚠️ **Ordre important** : `@nodefony/frontend` doit être déclaré AVANT les modules qui appellent `registerEntry()`. Sinon le service `frontend` n'est pas dans le DI Container au `onKernelBoot` du consumer → `ERROR @nodefony/frontend service unavailable`.

## Étape 2 — Créer le module applicatif

```bash
mkdir -p src/modules/shop-front/nodefony/{config,controller}
mkdir -p src/modules/shop-front/frontend/src
```

**`src/modules/shop-front/package.json`** : copier depuis `src/modules/test-frontend-react/package.json`, remplacer `test-frontend-react` par `shop-front`.

**`src/modules/shop-front/tsconfig.json`** et **`rolldown.config.ts`** : copier à l'identique depuis `test-frontend-react/`.

## Étape 3 — `index.ts` du module

```typescript
import { Kernel, Module } from "nodefony";
import { controllers } from "@nodefony/framework";
import type { FrontendService } from "@nodefony/frontend";
import config from "./nodefony/config/config";
import ShopFrontController from "./nodefony/controller/ShopFrontController";

@controllers([ShopFrontController])
class ShopFront extends Module {
  constructor(kernel: Kernel) {
    super("shop-front", kernel, import.meta.url, config);
  }

  override async onKernelBoot(): Promise<this> {
    const svc = this.kernel?.container?.get("frontend") as
      FrontendService | undefined;
    if (!svc) {
      this.log(
        "@nodefony/frontend service unavailable — wrong modules order?",
        "ERROR",
      );
      return this;
    }
    svc.registerEntry(this, {
      type: "react19",
      entry: "./frontend/src/main.tsx",
      root: "./frontend",
      outDir: "./public/dist",
      name: "shop-front",
    });
    return this;
  }
}

export default ShopFront;
```

## Étape 4 — Config du module

**`src/modules/shop-front/nodefony/config/config.ts`** :

```typescript
// Rien à surcharger pour le front : Vite reste en HTTP sur la boucle locale,
// relayé par Nodefony sur l'origine de la page. Servie sur
// https://localhost:5152/, la page charge ses scripts en HTTPS, sans réglage.
const config = {};

export default config;
```

## Étape 5 — Controller backend

**`src/modules/shop-front/nodefony/controller/ShopFrontController.ts`** :

```typescript
import { Controller, route, controller } from "@nodefony/framework";
import { Context } from "@nodefony/http";
import type { FrontendService } from "@nodefony/frontend";

@controller("/shop-front")
class ShopFrontController extends Controller {
  constructor(context: Context) {
    super("ShopFrontController", context);
  }

  @route("shop-react", { path: "/" })
  renderReact(): unknown {
    this.setContextHtml();
    const svc = this.context?.container?.get("frontend") as
      FrontendService | undefined;

    // Rien à faire pour la CSP : le service déclare les origines Vite au
    // firewall (`@nodefony/security`), qui émet UN seul en-tête. Réécrire
    // l'en-tête ici écraserait le nonce de la requête.

    // On propage le nonce CSP de la requête (sans lui, les balises émises
    // sont bloquées). En développement, les balises vers Vite sont relatives
    // à la page (`/_vite/<famille>/…`, relayées par Nodefony) : votre poste et
    // un navigateur en conteneur (ou une machine distante) chargent la même
    // page en même temps, sans rien à configurer.
    const viteTags =
      svc?.renderTags("shop-front", this.context?.cspNonce) ??
      "<!-- @nodefony/frontend not ready -->";

    return this.render(`<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Shop Front</title>
    ${viteTags}
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>`);
  }

  @route("shop-api-data", { path: "/api/data" })
  apiData() {
    return this.renderJson({ ts: Date.now(), module: "shop-front" });
  }
}

export default ShopFrontController;
```

> **Le CSP est posé par le pare-feu** (`@nodefony/security`), qui déclare les origines Vite et émet un seul en-tête avec le nonce de la requête : ne le réécrivez pas dans le contrôleur, vous écraseriez le nonce.

## Étape 6 — Frontend React

**`src/modules/shop-front/frontend/index.html`** (optionnel mais recommandé pour test Vite standalone) :

```html
<!doctype html>
<html lang="en">
  <head>
    <title>Shop Front — Vite standalone</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

**`src/modules/shop-front/frontend/src/main.tsx`** :

```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";

const rootEl = document.getElementById("root");
if (!rootEl) throw new Error("#root not found");
createRoot(rootEl).render(
  <StrictMode>
    <App />
  </StrictMode>,
);
```

**`src/modules/shop-front/frontend/src/App.tsx`** :

```tsx
import { useEffect, useState } from "react";

interface ApiData {
  ts: number;
  module: string;
}

export function App() {
  const [data, setData] = useState<ApiData | null>(null);

  useEffect(() => {
    // fetch relatif → origine de la page, donc directement Nodefony.
    fetch("/shop-front/api/data")
      .then((r) => r.json())
      .then((json) => setData((json.result ?? json) as ApiData));
  }, []);

  return (
    <div style={{ padding: 24 }}>
      <h1>Shop Front</h1>
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>loading…</p>}
    </div>
  );
}
```

## Étape 7 — Build + test

```bash
cd src/modules/shop-front && npm run build
cd ../../.. && npx tsc --noEmit
```

Puis lancer le serveur dev :

```bash
# Via le skill (recommandé) :
/start-server

# Ou manuellement :
npx nodefony development
```

Logs attendus :

```
INFO frontend : registered entry: shop-front (react19) from "shop-front"
INFO frontend : [vite] VITE v8.x ready in XXX ms
INFO frontend : [vite] ➜  Local:   https://127.0.0.1:5173/
INFO frontend : vite dev server ready on 127.0.0.1:5173
```

Naviguer sur `https://127.0.0.1:5152/shop-front/`. La page React s'affiche, le `Backend ping` doit retourner du JSON.

> Première visite HTTPS : le browser demande d'accepter le cert self-signed sur `127.0.0.1:5152` ET sur `127.0.0.1:5173` (origines distinctes). Tu peux installer la CA root Nodefony (`nodefony/config/certificates/ca/nodefony-root-ca.crt.pem`) dans ton trousseau pour ne plus avoir le prompt.

## Étape 8 — Tester le HMR

Édite `frontend/src/App.tsx`, change un texte, sauvegarde. Sans rafraîchir la page :

- Le texte change dans le browser
- Si tu avais un state local (`useState`), il est **préservé** (c'est la signature du HMR React Fast Refresh)
- Le log Nodefony affiche : `[vite] (client) hmr update /src/App.tsx`

Si la page reload entièrement (state perdu) → c'est un full reload, pas du HMR. Causes possibles :

- Le preamble React n'est pas injecté (vérifie que tu utilises `svc.renderTags()`, pas des `<script>` à la main)
- Le canal WSS HMR ne s'est pas établi (cert HTTPS refusé sur 5173 ?)
- L'app crash silencieusement → reload comme fallback (regarde la console DevTools)

## Étape 9 — Build production

```bash
npx nodefony frontend:build
```

Génère `src/modules/shop-front/public/dist/manifest.json` + assets fingerprintés. En production, `svc.renderTags()` lit le `manifest.json` et injecte les assets fingerprintés (JS, CSS, preloads), préfixés par `publicPath` ou `assetBaseUrl`.

## Pièges récurrents (à connaître)

| Symptôme                                               | Cause                            | Fix                                                            |
| ------------------------------------------------------ | -------------------------------- | -------------------------------------------------------------- |
| `ERROR @nodefony/frontend service unavailable`         | Ordre du manifeste `modules`     | Déclarer `@nodefony/frontend` AVANT le module consumer         |
| Page blanche, `Refused to load script ... blocked:csp` | Un controller réécrit le CSP     | Le laisser au firewall : il déclare déjà les origines Vite     |
| Assets sur `127.0.0.1` depuis une AUTRE machine        | Hôte hors `trustedHosts`         | L'y ajouter en dev : la même liste ouvre 421, Vite, CSP, rendu |
| `Unexpected token '<'` sur `fetch("/api/...")`         | page ouverte sur le port de Vite | Ouvrir la page par Nodefony                                    |
| `@vitejs/plugin-react can't detect preamble`           | Preamble manquant                | Toujours utiliser `svc.renderTags(name)`                       |
| Cache navigateur après modif CSP                       | Browser cache                    | **Cmd+Shift+R** (hard reload)                                  |
| Modules React introuvables                             | Cert HTTPS Vite refusé sur 5173  | Visiter `https://127.0.0.1:5173/` une fois et accepter le cert |

## 📖 Lexique

| Terme                        | Ce que c'est                                                                                                                                                |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Point d'entrée** (_entry_) | Ce qu'un module déclare à `@nodefony/frontend` : un nom, un fichier source, les chemins d'API à mandater. Tout part de là.                                  |
| **HMR**                      | _Hot Module Replacement_ — Vite remplace à chaud le module modifié dans la page ouverte, sans rechargement ni perte d'état.                                 |
| **Superviseur Vite**         | Le processus qui lance Vite, le surveille et le relance s'il tombe. Il vit dans l'application, pas dans votre terminal.                                     |
| **CSP**                      | _Content Security Policy_ — l'en-tête qui dit au navigateur quelles origines ont le droit de fournir des scripts. En développement, elle doit inclure Vite. |
| **`renderTags`**             | Le rendu des balises `<script>`/`<link>` de votre point d'entrée : elles pointent vers Vite en développement, vers les fichiers bâtis en production.        |

## 🧪 Tests & couverture

Les chiffres exacts vivent dans la carte de l'aperçu, régénérée depuis vitest — jamais figés ici.

| Type | Où | Ce qui est prouvé |
| --- | --- | --- |
| Unitaires | `@nodefony/frontend` `unit/ViteConfigGenerator.test.ts`, `unit/ViteBuilder.test.ts`, `unit/templateHelperOrigin.test.ts` | la configuration Vite engendrée, le build, l'origine des balises rendues |
| Unitaires (sécurité) | `unit/cspBeforeVite.test.ts`, `unit/devAssetRelay.test.ts` | la politique de sécurité inclut Vite, et Vite est relayé sur l'origine de la page |
| Unitaires (résilience) | `unit/viteSupervisorStop.test.ts`, `unit/pidFromNetstat.test.ts` | l'arrêt de l'arbre de processus, la reprise d'un port occupé |
| Intégration | `integration/ViteProcessSupervisor.test.ts`, `integration/frontend-build.test.ts` | le superviseur avec un vrai Vite, et un build complet |

## 🔗 Pour aller plus loin

- ⬆️ **Retour au hub** : [Guides](../) · [Toute la documentation](../../)
- 📚 **L'interface publique complète** :
  [`@nodefony/frontend`](../../modules/frontend/)
- 🛟 **Ce qui se passe quand Vite tombe** (relance, port occupé, sonde de vie) :
  [résilience du superviseur](../../modules/frontend/#résilience--ce-qui-se-passe-quand-vite-tombe)
- 🧩 **Exemples vivants dans le dépôt** : `src/modules/test-frontend-react/` — et ses trois frères
  `test-frontend-vue/`, `test-frontend-angular/`, `test-frontend-svelte/`, qui montrent le même
  montage avec chacun des quatre choix.
- 🛠️ **La console d'administration** consomme elle-même `@nodefony/frontend` :
  [`@nodefony/studio`](../../modules/studio/)
- 📖 [Lexique général](../../lexique/) du framework.
