Nodefony 10.0.0-beta.5
Framework Node.js fullstack — HTTP & WebSocket, même contexte

Générer du code — `nodefony create`

stablemis à jour 2026-10-02

Ce que cette page vous donne : comment créer une application, un module, un contrôleur, un frontend ou une entité sans écrire le squelette à la main — et surtout comment voir ce qui va changer avant que ça change. Elle s’adresse autant à la personne qui tape la commande qu’à l’agent qui l’appelle : les deux passent par la même porte, et cette porte sait se décrire.

📍 Documentation › Guides › Générer du code

Schéma général#

Trois portes, un moteur. Le moteur ne parle jamais au terminal et n’ouvre jamais de connexion : il transforme des réponses validées en fichiers. Tout ce qu’il écrit passe d’abord par une transaction — c’est elle qui rend la simulation possible, et qui garantit qu’un refus ne laisse rien derrière lui.

Lexique#

Terme Ce que ça veut dire
Scaffold (échafaudage) Génération d’un squelette de code prêt à tourner. Le résultat vous appartient : vous l’éditez, il n’est jamais régénéré par-dessus.
Gabarit (template) Fichier modèle rendu par le moteur. Ils vivent dans src/nodefony/templates/, un dossier par type de création.
Spec Description déclarative des questions d’un type (nom, valeurs permises, défaut). Elle est en JSON pur : les trois portes lisent la même.
Cible (target) Où le code est écrit dans un projet existant : l’application racine, ou l’un de ses modules locaux (modules/<nom>/).
Câblage (wiring) L’édition qui rend le code généré VIVANT : ajouter la classe au décorateur @controllers([…]), l’entité à @entities([…]), le module au manifeste.
Transaction Les écritures sont retenues en mémoire et versées sur le disque en une seule fois, à la toute fin. Rien n’est écrit tant qu’une étape peut encore refuser.
Simulation (dry-run) La même exécution, sans le versement final. Rend le plan : fichiers à créer, et le diff de ceux qui seraient réécrits.

Qu’est-ce que c’est ?#

Un générateur de code est un cuisinier qui prépare votre mise en place. Il ne cuisine pas le plat à votre place : il sort les bons ustensiles, coupe les légumes dans le bon sens et pose tout à portée de main. Ce que vous obtenez est un point de départ correct, pas une boîte noire — vous le modifiez librement dès la seconde qui suit.

L’intérêt n’est pas de gagner les dix minutes qu’aurait pris la frappe. C’est que le squelette soit conforme : les bons imports, la bonne structure de dossiers, la validation branchée au bon endroit, le câblage fait. Ces détails-là ne s’inventent pas, ils se copient — et une copie faite à la main copie aussi les erreurs de l’exemple qu’elle a sous les yeux.

La vision Nodefony#

Le générateur est un outil qu’on appelle, pas une collection de fichiers qu’on imite.

C’est une différence de nature. Un agent — ou un développeur pressé — qui imite un fichier existant reproduit ce que cet exemple avait de particulier, y compris ce qui a vieilli. Un appel, lui, part de la spec courante : getScaffoldSpec() (spec.ts#getScaffoldSpec) décrit les types, leurs questions et leurs valeurs permises, et resolveAnswers() (engine.ts#resolveAnswers) refuse tout ce qui sort de cette description. Le générateur peut donc dire ce qu’il attend, et l’appelant n’a rien à deviner.

Le second parti pris est qu’un refus ne doit rien coûter. Un scaffold est une suite d’étapes dont plusieurs peuvent échouer tard : le nom de classe est déjà pris, l’index.ts n’a pas de décorateur où insérer, un gabarit est cassé. Toutes les écritures passent donc par une transaction (ScaffoldWriter, writer.ts#ScaffoldWriter), et seul le scaffold racine la verse sur le disque (commit(), writer.ts#ScaffoldWriter.commit). Refuser redevient un non-événement.

Démarrage rapide#

# Une application neuve, hors de tout projet. Le canal `@beta` se nomme tant que
# la 10 n'est pas stable : sans lui, npm sert l'ancienne version 7.
npm create nodefony@beta mon-app -- --preset minimal
cd mon-app && npm run dev

Dans une application existante, on crée des briques :

# Un contrôleur HTTP + WebSocket dans la même classe.
npx nodefony create controller blog --kind hello

# Une entité et toute sa chaîne : table, schémas d'entrée, service, contrôleur REST, tests.
npx nodefony create entity Article title:string body:text views:int

Le contrôleur produit par la première commande est exactement celui-ci — c’est le même gabarit qui sert le contrôleur d’accueil d’une application neuve :

// nodefony/controllers/BlogController.ts — extrait, compile tel quel
import {
  route,
  controller,
  Controller,
  CurrentUser,
} from "@nodefony/framework";
import type { ContextType } from "@nodefony/http";
import type { IUser } from "@nodefony/user";

@controller("/api/blog")
class BlogController extends Controller {
  constructor(context: ContextType) {
    super("blog", context);
  }

  @route("blog-index", { path: "", method: "GET" })
  async index(@CurrentUser() user?: IUser) {
    // Un visiteur non authentifié (anonyme compris) donne `undefined`.
    const authenticated = !!user?.identifier;
    return this.renderJson({
      hello: "blog",
      pid: process.pid,
      who: authenticated ? user!.identifier! : "anonyme",
    });
  }

  // MÊME classe, MÊME décorateur : seul le transport déclaré change.
  @route("blog-echo", {
    path: "/echo",
    requirements: { methods: ["WEBSOCKET"] },
  })
  async echo(message: string | Buffer | null) {
    if (!message) return this.renderJson({ handshake: true });
    return this.renderJson({ echo: message.toString() });
  }
}

export default BlogController;

Ce qu’on observe :

curl http://127.0.0.1:5151/api/blog
# {"hello":"blog","pid":12345,"who":"anonyme"}

Les sept choses qu’on peut créer#

Type Ce que ça pose Où
app Un projet complet : configuration, environnement typé, contrôleur d’accueil, tests, outillage. Deux presets, quatre frontends, quatre bases SQL ou MongoDB. Un dossier neuf
module Un workspace npm sous modules/<nom>/, déclaré dans les workspaces et le manifeste modules de l’application. Dans le projet courant
controller Une classe de contrôleur, dans l’une des six saveurs (hello, rest, duplex, realtime, example, mcp), câblée à sa cible. --role ROLE_X le réserve à une habilitation. --kind mcp ouvre une porte MCP de l’application (--expose local, oauth ou mixte). Application ou module
front Un frontend Vite : coquille HTML, point de montage, contrôleur de page, déclaration d’entrée. Application ou module
entity La chaîne de persistance complète : table du dialecte (ou schéma Mongoose sur MongoDB), schémas d’entrée, service CRUD, contrôleur REST + socket, tests. Application ou module
service Un service injectable (@injectable) : la logique métier, réutilisable par un contrôleur, une commande ou un autre module. Peut injecter un service existant de la cible. Application ou module
command Une commande CLI nodefony <nom>:<action>, avec la phase de démarrage où elle s’exécute. Application ou module

Le détail des drapeaux de chaque type est dans nodefony create --help, et sous forme lisible par une machine dans --describe-json (voir plus bas).

Les champs d’une entité, moteur par moteur#

nom:type[?][=défaut][:index|:unique] — non nul par défaut ; ? rend facultatif, =valeur fixe un défaut littéral, :unique pose l’unicité, :index un index. Le ! est refusé : un champ est déjà obligatoire. Une relation s’écrit <champ>:ref:<Entité>. L’entité et la cible en PascalCase (Post, ref:User), le champ en camelCase ; une faute de casse, ou un type d’un autre outil (boolean, integer), est refusé avec la forme juste.

Type Ce que ça produit SQLite PostgreSQL MySQL / MariaDB MongoDB
string(n) texte court, borné (255) text ¹ varchar(n) varchar(n) String + maxlength
text texte long text text text String
int entier integer integer int Number
float nombre à virgule real double double Number
decimal(p,s) décimal EXACT (voyage chaîne) numeric numeric(p,s) decimal(p,s) String
bool booléen integer (0/1) boolean boolean Boolean
json document libre text (JSON) jsonb json Mixed
date horodatage (ms) integer (ms) timestamptz(3) datetime(3) Date
uuid identifiant text uuid varchar(36) String
char(n) longueur EXACTE text ¹ char(n) char(n) String, n exact
enum(a,b) valeurs admises ² text varchar(255) varchar(255) String + enum
ref:<Entité> relation ³ clé étrangère clé étrangère clé étrangère ObjectId + ref

¹ SQLite n’applique aucune longueur : c’est le schéma d’entrée (Zod) qui borne, sur TOUS les transports. ² Même colonne partout, sans type SQL nommé (qui exigerait une migration) : le type TypeScript et le schéma Zod bornent les valeurs. ³ En SQL, la colonne prend le type de la clé visée (uuid, entier pour --id serial, texte pour User) ; en MongoDB, aucune clé étrangère.

Sur MongoDB, la même commande écrit une entité document : la clé est l’_id natif, servie en id, et aucune migration n’existe. Les options propres au SQL (--table, --column-case, --id-name, --dialect, --id, --index, --unique) y sont refusées en le disant. Cette table est confrontée au générateur par un test : elle dit ce que CETTE version écrit, et --describe-json (champ context.columnTypes) le rend pour une machine.

Voir avant d’écrire#

Créer une brique dans un projet existant ne fait pas qu’ajouter des fichiers : ça réécrit les vôtres — l’index.ts gagne un import et une entrée de décorateur, le package.json une dépendance, la configuration une ligne de manifeste. --dry-run (ou -n) montre tout ça sans rien toucher :

npx nodefony create entity Article title:string --dry-run
🔍 simulation (--dry-run) — RIEN n'a été écrit

  5 fichier(s) à créer, 1 à réécrire

Créés :
  + nodefony/entity/Article.ts
  + nodefony/entity/Article.schema.ts
  + nodefony/service/ArticleService.ts
  + nodefony/controllers/ArticleController.ts
  + tests/article.test.ts

Réécrit : index.ts
  + import { ArticleEntity } from "./nodefony/entity/Article";
  + @entities([ArticleEntity])
  - @controllers([HelloController])
  + @controllers([HelloController, ArticleController])

Les créations sont listées à plat — leur contenu n’écrase rien. Les réécritures montrent leur diff : c’est là que la simulation a de la valeur.

La simulation traverse le moteur en entier, gardes comprises : un scaffold qui sera refusé l’est déjà ici, avec le même message. Elle ne peut donc pas promettre un résultat que l’exécution refuserait.

Un refus n’écrit rien#

C’est une garantie, pas une intention. Quelle que soit l’étape qui refuse — nom de classe déjà utilisé, @controllers([…]) introuvable dans la cible, gabarit cassé, dépendance manquante — le projet est dans l’état exact où vous l’avez laissé.

npx nodefony create entity Article title:string
# create: ArticleEntity est déjà référencé dans index.ts — choisis un autre nom d'entité
# → aucun fichier créé, aucun fichier modifié

La garantie tient à la transaction, pas à la position des vérifications dans le code : runScaffold() (engine.ts#runScaffold) ouvre la transaction, chaque étape y écrit, et le versement n’a lieu qu’après la dernière. Une garde ajoutée demain est automatiquement sûre, où qu’elle soit placée.

ℹ️ Note

Ce n’est pas de l’atomicité au sens du système de fichiers : aucune API portable ne l’offre pour un arbre de fichiers. Une panne disque en plein versement laisserait un résultat partiel. Ce que la transaction supprime, c’est le cas qui arrivait vraiment : une décision du moteur produisant un projet à moitié modifié.

Piloter le générateur depuis un agent ou un script#

Deux drapeaux suffisent, et ils fonctionnent pour les sept types.

Se faire décrire l’outil — types, questions, valeurs permises, défauts, capacités de l’environnement, et les cibles du projet courant si l’on est dans une application :

npx nodefony create --describe-json           # tout le catalogue
npx nodefony create entity --describe-json    # ce type seulement

Répondre en JSON, depuis un fichier ou l’entrée standard :

echo '{"name":"Article","fields":"title:string body:text"}' \
  | npx nodefony create entity --answers-json - --dry-run

Les drapeaux l’emportent sur le fichier : celui-ci porte le gros de la demande, le drapeau est la retouche de l’appel.

❗ Important

Une clé absente de la spec est refusée (code de sortie 64), pas ignorée. resolveAnswers() ne conserve que les clés déclarées : un "prest" écrit à la place de "preset" produirait un projet différent de celui demandé, sans un mot. Une personne relit le résultat ; un appelant automatique, non (readAnswersJson(), create.ts#readAnswersJson).

Combinés, ces trois drapeaux forment une boucle sûre pour un agent : se décrire (--describe-json), proposer (--answers-json … --dry-run), puis exécuter.

Depuis Studio#

L’écran « Créer » de l’administration web (/nodefony/create) passe par le même moteur : il n’existe pas de seconde implémentation qui pourrait dériver. Il sert le formulaire à partir de la spec (/nodefony/studio/api/create/spec), montre la préview du plan avant d’exécuter (preview(), ScaffoldService.ts#ScaffoldService.preview), puis streame la progression du travail — écriture, installation des dépendances, construction — sur un canal temps réel. Les étapes partent par le gestionnaire de paquets de l’application (npm, pnpm, yarn ou bun, lu à son fichier de verrou), comme en ligne de commande.

⚠️ Attention

Ces routes écrivent sur le disque et lancent le gestionnaire de paquets. Elles répondent 403 hors développement, quel que soit le rôle : le refus est prononcé côté serveur, pas en masquant une entrée de menu. Les étapes exécutables sont une liste fermée — le client coche un identifiant d’étape, il n’envoie jamais une ligne de commande.

Ce que le générateur ne fait pas#

Pièges#

Symptôme Cause Correction
aucun projet Nodefony ici Les types in-project remontent aux nodefony.config.ts depuis le dossier courant — vous êtes en dehors. Se placer dans l’application, ou créer d’abord nodefony create app.
X est déjà référencé dans index.ts Un contrôleur ou une entité porte déjà ce nom dans la cible. Choisir un autre nom. Rien n’a été écrit : le projet est intact.
@controllers([...]) introuvable L’index.ts de la cible n’a plus le décorateur où insérer la classe. Le message donne l’édition exacte à faire à la main. Le fichier généré n’a pas été posé — pas d’orphelin.
Le module créé n’est pas chargé au démarrage Un module est un workspace de l’application : sans installation, le lien n’existe pas et le kernel ne le résout pas par son nom. Lancer l’installation à la racine de l’application (npm install, ou pnpm install, yarn, bun install), puis npm run build (ou son équivalent).
npm run typecheck échoue en TS2882 sur un import de feuille de style TypeScript ne connaît pas les fichiers que Vite sait importer. "types": ["node", "vite/client"] dans le tsconfig.json — le scaffold le pose déjà quand il génère un front.
Une entité modifiée ne change pas la table En développement, seule une colonne qui accepte le vide est rattrapée ; une colonne obligatoire ne l’est jamais. nodefony orm:reset en développement, ou nodefony orm:generate --name <nom> puis nodefony orm:migrate — jamais à la main.

Tests#

Le générateur est couvert par quatre suites unitaires — aucune n’a besoin d’un serveur. Les compteurs exacts vivent dans la carte de l’aperçu, régénérée depuis vitest, jamais figés ici.

Suite Ce qu’elle verrouille
src/nodefony/src/tests/create.test.ts Analyse des drapeaux, spec déclarative, moteur sur les sept types (2 presets × 4 frontends), front interactif sur flux factices, mode machine, simulation, intégrité après refus, catalogue de versions
src/nodefony/src/tests/entityFields.test.ts Grammaire des champs d’entité et sa traduction dans les trois dialectes — module pur, testable sans disque
src/nodefony/src/tests/scaffoldDestination.test.ts Où une application a le droit de naître : recomposition sous une racine autorisée, refus de toute traversée
@nodefony/studio/nodefony/tests/unit/scaffoldService.test.ts Refus hors développement, préview qui ne touche pas au disque, distinction créé/réécrit, refus du moteur répercuté

Le contrôle qui compte le plus est celui de l’intégrité : il prend une empreinte complète du projet, provoque un refus, et exige que rien n’ait bougé — ni ajout, ni suppression, ni octet modifié. Un assert.throws seul ne dit rien de ce que le moteur a écrit avant de lever.

cd src/nodefony && npx vitest run src/tests/create.test.ts
cd src/packages/@nodefony/studio && npx vitest run

Pour aller plus loin#