Nodefony
Framework Node.js fullstack — HTTP & WebSocket, même contexte

nodefony — le cœur du framework

stablenodefony (cœur)mis à jour 2026-07-19

Le socle dont tous les autres paquets dépendent : @nodefony/http pour parler HTTP, @nodefony/framework pour router, @nodefony/security pour garder, les adaptateurs de base de données pour persister. Il apporte cinq choses et rien d’autre : une classe de base commune à tout composant, un conteneur de services, un cycle de vie (le kernel et ses modules), une journalisation structurée, et un contexte de requête qui suit le code jusque dans ses recoins asynchrones. Il est aussi la seule brique du framework qui tourne dans le navigateur.

📍 DocumentationCœur — @nodefony/core

❗ Important

Le paquet npm s’appelle nodefony, pas @nodefony/core. C’est le piège d’identité numéro un : on lit @nodefony/core partout — dans cette documentation, dans les fiches de module, dans Studio — parce que c’est le nom interne de la brique. Mais dans un package.json et dans un import, c’est nodefony tout court :

import { Module, Service, Nodefony } from "nodefony"; // ✅ le vrai nom du paquet
import { Module } from "@nodefony/core"; // ❌ ce paquet n'existe pas sur npm

L’héritage vient de l’ère JavaScript du framework, et le renommage serait cassant pour tout l’écosystème. Deux autres pièges d’import vivent au même endroit : il n’y a pas d’export par défaut (import nodefony from "nodefony" échoue — écrire import { Nodefony } from "nodefony"), et la classe d’erreur s’appelle nodefonyError, pas Error, pour ne pas entrer en collision avec celle du langage.

🧭 Par où commencer#

Quatre parcours selon ce que tu viens faire. L’ordre compte : chaque étape suppose la précédente.

Je découvre le framework — comprendre le modèle avant d’écrire une ligne.

  1. Kernel, Module et CliKernelcommence ici. Trois classes suffisent à décrire une application Nodefony : ce que tu écris, ce qui te porte, et la façade qui relie les deux.
  2. Service — la classe dont hérite absolument tout, kernel et modules compris. Comprendre Service, c’est comprendre pourquoi chaque composant sait s’injecter, émettre des événements et écrire dans le journal sans qu’on le lui apprenne.
  3. Le cycle de boot — l’ordre d’allumage réel, les hooks où brancher son code, et l’arrêt propre.
  4. Vue d’ensemble du framework — où se pose le cœur par rapport aux autres modules.

J’écris mon premier module — le chemin le plus court vers du code à toi qui tourne.

  1. Démarrage rapide — le manifeste de modules, un module, un service injecté. Copiable tel quel.
  2. Injection et portées — déclarer un service, l’injecter, et surtout choisir sa portée : partagé pour tout le processus, ou recréé à chaque requête. C’est la page canonique du conteneur de dépendances.
  3. Kernel, Module et CliKernel — les hooks de cycle de vie (onKernelBoot, onKernelReady), et ce qu’on peut supposer à chacun.
  4. ConfigurationdefineConfig, use(), l’environnement typé, et la validation au boot.

Je veux du temps réel dans mon front — le différenciateur du framework, vu du navigateur.

  1. Le client isomorphe — le même paquet nodefony, importé côté navigateur. La socket, les canaux, la cadence adaptative, les rôles évalués côté client.
  2. Les hooks React — la même socket, en idiomes React : abonnement, désabonnement au démontage, comptage de références. Ne câble pas le client à la main si tu es en React.
  3. Les composables Vue — la même surface, en idiomes Vue : un plugin sur l’application, puis des composables dont la portée rend l’abonnement. Même chose si tu es en Vue.
  4. Les fonctions d’injection Angular — la même surface encore, en idiomes Angular : un fournisseur dans les providers, puis des fonctions qui rendent des signals et libèrent l’abonnement à la destruction du composant. La connexion y est ouverte hors zone.
  5. La réactivité Svelte — la même surface, une dernière fois : une configuration de module, puis des valeurs qui se lisent .current et dont l’abonnement est rendu par le système d’effets. Aucune rune n’est publiée, et l’abonnement y est paresseux.
  6. @nodefony/realtime — le serveur en face : le hub qui distribue les canaux, et le backplane quand l’application passe à plusieurs répliques.

Je veux tracer une requête de bout en bout — l’enquête, quand quelque chose s’est mal passé.

  1. Journalisation — la structure d’une entrée de journal, le tampon circulaire qui garde les dernières sans jamais grossir, et les transports qui l’expédient ailleurs.
  2. RequestContext — ce qui fait qu’un identifiant de requête se retrouve dans une ligne de journal écrite par un service appelé six niveaux plus bas, sans l’avoir passé en paramètre.
  3. Le pipeline de requête — le trajet complet, de l’octet reçu à l’octet renvoyé, HTTP comme WebSocket.

🗂️ Les pages du cœur#

Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce qu’on y trouve.

Page Ce qu’elle résout Tu en as besoin quand…
Service la classe de base : injection, événements, journal tu écris n’importe quel composant
Journalisation ce qui s’écrit, où ça part, ce qu’on garde en mémoire tu instrumentes, ou tu enquêtes
Kernel & Module l’API du cœur : démarrer, brancher, étendre tu écris un module ou une commande
CLI piloter le framework en ligne de commande tu lances, construis, échafaudes, ou étends
Tests éprouver ce que tu écris — unitaire, bout en bout tu lances npm test pour la première fois
RequestContext suivre une requête à travers l’asynchrone tu corrèles des journaux, ou tu lis l’identité
Client isomorphe le même paquet, dans le navigateur tu écris du front qui parle au serveur
Hooks React la socket en idiomes React ton front est en React
Composables Vue la socket en idiomes Vue ton front est en Vue 3
Injection Angular la socket en idiomes Angular ton front est en Angular
service La brique de base : kernel, modules, contrôleurs et adaptateurs de base de données en héritent tous. Trois capacités obtenues gratuitement — être injectable, émettre et écouter des événements, écrire dans le journal — et le cycle de vie qui va avec, nettoyage compris. à lire en premier : elle rend les autres évidentes syslog Une entrée de journal n'est pas une chaîne de caractères : c'est un enregistrement typé, horodaté, attribué à un module et à une sévérité. Avec le tampon circulaire qui garde les dernières à coût constant, et les transports vers un fichier, un agrégateur ou l'écran de Studio. tu instrumentes, ou tu enquêtes kernel Kernel (ce qui te porte), Module (ce que tu écris), CliKernel (la même chose pour une commande en ligne), et la façade Nodefony qui te rend le kernel courant depuis n'importe où. la référence des objets — le récit du démarrage vit dans le cycle de boot testing Le sous-chemin `nodefony/testing` et le harnais livré avec chaque application créée : un module jetable pour éprouver un service seul, la lecture du port de l'application qui tourne, et un exemplaire à part pour observer ce qui ne se voit qu'au démarrage. ton premier `npm test` repose dessus cli Le binaire `nodefony` : démarrer en développement, construire, lancer en production ou en cluster, échafauder un projet ou un module, et accueillir les commandes que tes propres modules ajoutent. Chaque commande choisit jusqu'où booter le kernel. tu pilotes, tu échafaudes, ou tu ajoutes ta commande request-context Plusieurs requêtes traversent le même processus en même temps, et chacune garde son identité à travers les await, les rappels et les minuteries. Ce qu'on peut y ranger — identifiant de requête, utilisateur, trace distribuée — et le piège des écouteurs qui se déclenchent plus tard. tu corrèles des journaux, ou tu lis l'identité client Le cœur est isomorphe : un sous-ensemble compile pour le navigateur, ce qui donne la socket temps réel côté client sans importer un second paquet. La connexion et sa reprise, les canaux, la cadence auto-ajustée, l'évaluation des rôles côté interface. le même paquet, dans le navigateur react-hooks Le fournisseur de contexte et les hooks : état de connexion, abonnement à un canal, données du dernier message, flux du journal, notifications. Dix composants qui écoutent dix canaux ouvrent une connexion, pas dix. tout le désabonnement au démontage est géré vue-composables Un plugin sur l'application, puis les mêmes capacités en composables : état, identité, canaux, cadence adaptative, journal, notices. La portée du composant rend l'abonnement — il n'y a rien à libérer, et une fuite d'abonnement ne se voit jamais à l'écran. ton front est en Vue 3 angular-services Un fournisseur dans les providers de l'application, puis les mêmes capacités en fonctions d'injection qui rendent des signals. La connexion est ouverte hors zone — sans quoi, avec zone.js, chaque trame reçue relancerait une détection de changements sur toute l'application. ton front est en Angular svelte-reactivite Une configuration de module, puis les mêmes capacités en valeurs qui se lisent .current. Aucune rune n'est publiée — une bibliothèque qui en publierait imposerait au consommateur de la compiler. L'abonnement est pris à la première lecture et rendu quand plus personne ne lit. ton front est en Svelte 5

ℹ️ Note

L’injection de dépendances n’a pas de page ici, et c’est voulu. Le conteneur, les portées et les décorateurs @injectable / @inject / @services sont documentés une seule fois, au niveau transverse : Injection et portées. Le sujet traverse tous les modules — le confiner dans la doc du cœur obligerait chaque autre module à le répéter, et trois vérités valent moins qu’une.

🧩 Ce que le cœur apporte#

Quatre propriétés, toutes vérifiables dans le code — c’est ce qui distingue le cœur d’une simple boîte à outils.

Tout composant partage la même classe de base. Service (Service.ts:43) porte à la fois le conteneur d’injection, le bus d’événements et l’accès au journal. Conséquence pratique : un module, un contrôleur et un adaptateur de base de données s’observent, se configurent et se nettoient de la même façon. Il n’y a pas de composant « à part » dans une application Nodefony.

Le kernel possède les modules, jamais l’inverse. Kernel.boot() (Kernel.ts:799) charge le manifeste, construit les modules, puis fait passer tout le monde par les mêmes phases. Un Module (Module.ts:60) déclare s’il est critique — un module non critique dont le démarrage échoue n’emporte pas le processus, il annonce sa dégradation et le reste continue. Les commandes en ligne empruntent le même chemin par CliKernel (CliKernel.ts:84).

Le journal est structuré, borné, et il part où tu veux. Syslog (Syslog.ts:628) écrit des enregistrements typés plutôt que des chaînes, et les conserve dans un tampon circulaire (CircularBuffer, Syslog.ts:273) : les dernières entrées restent lisibles à chaud, sans que la mémoire enfle avec le temps de fonctionnement. Les transports décident ensuite de la destination — sortie standard, fichier, agrégateur.

Le contexte d’une requête voyage sans être passé en paramètre. RequestContext (RequestContext.ts:115) s’appuie sur le stockage asynchrone de Node : un service appelé profondément peut lire l’identifiant de requête ou l’utilisateur courant sans que personne n’ait threadé d’argument. C’est ce qui rend les journaux corrélables et l’identité disponible partout.

Enfin, le cœur est le seul paquet du framework qui tourne aussi côté navigateur : la même distribution publie un build client, ce qui permet de partager du code — et surtout le client temps réel — entre le serveur et l’interface. Le détail des sous-chemins est en Surface publique.

🚀 Démarrage rapide#

Vu depuis une application créée par nodefony create app. Deux fichiers suffisent à voir le cœur travailler : le manifeste qui décide ce qui est chargé, et un module qui apporte son propre service.

1. Déclarer ce que l’application charge#

nodefony.config.ts est l’orchestrateur : un seul fichier, et seulement les écarts aux valeurs d’usine du framework. Le tableau modules est ordonné — c’est l’ordre de chargement.

// nodefony.config.ts — à la racine de l'application
export default defineConfig((ctx) => ({
  // Un conteneur doit écouter toutes les interfaces : un port publié par
  // l'orchestrateur n'atteint jamais une écoute limitée à la boucle locale.
  domain: ctx.isProd ? "0.0.0.0" : "127.0.0.1",
  // Le par-environnement passe par `ctx`, jamais par un fichier parallèle.
  log: { debug: ctx.isProd ? [] : "*" },
  modules: [
    "@nodefony/http",
    "@nodefony/framework",
    // `use()` colocalise la configuration d'un module avec son chargement.
    use("@nodefony/realtime", { backplane: { driver: "cluster" } }),
  ],
}));

2. Écrire un module et son service#

Un module est une classe qui étend Module ; un service est une classe qui étend Service. Le décorateur @services enregistre le second auprès du premier — le conteneur s’occupe de la construction et des dépendances.

// nodefony/modules/catalog/index.ts
import { Module, Service, injectable, services } from "nodefony";
import type { Kernel, Container } from "nodefony";

// `@injectable` rend la classe constructible par le conteneur. La portée par
// défaut est partagée : une seule instance pour tout le processus.
@injectable()
class CatalogService extends Service {
  constructor(container: Container) {
    // Le nom est aussi celui qui apparaît dans le journal — choisis-le lisible.
    super("catalog", container);
  }

  find(sku: string): string {
    // Hérité de Service : pas d'importation, pas d'injection de logger.
    this.log(`recherche de ${sku}`, "INFO");
    return sku;
  }
}

// `critical = false` : si ce module échoue à démarrer, l'application monte quand
// même et la dégradation est annoncée. Statique, car lue avant les initialiseurs.
@services([CatalogService])
class CatalogModule extends Module {
  static override critical = false;

  constructor(kernel: Kernel) {
    super("catalog", kernel, import.meta.url, {});
  }

  // Le kernel est prêt : les autres modules et leurs services existent.
  override async onKernelReady(): Promise<this> {
    this.log("catalogue prêt", "INFO");
    return this;
  }
}

export { CatalogService };
export default CatalogModule;

Ce qu’on observe ensuite :

  1. Au démarrage, chaque phase du kernel est annoncée dans le journal, module par module — c’est le premier endroit à regarder quand quelque chose ne se charge pas.
  2. Les deux appels à this.log(…) sortent avec le nom catalog en identifiant de message : c’est ce qui permet de filtrer par module dans l’écran Journaux de Studio.
  3. CatalogService devient injectable ailleurs sous le nom catalog — un contrôleur le reçoit sans jamais l’instancier lui-même. La mécanique complète est en Injection et portées.

🏛️ Place dans le framework#

Chaque couche ne dépend que de celles du dessous : @nodefony/framework connaît @nodefony/http, jamais l’inverse — ce serait un cycle. Le cœur, lui, ne connaît personne : c’est ce qui lui permet d’être aussi le paquet qu’on importe dans le navigateur.

🧰 Surface publique#

Le paquet publie plusieurs sous-chemins (subpaths). Chacun est une porte d’entrée distincte, et c’est le sous-chemin qui décide de ce qui atterrit dans ton bundle.

<!-- prettier-ignore -->

Import Ce qu’on y trouve Où ça tourne
nodefony Kernel, Module, Service, Container, Syslog, Pdu, defineConfig, use, les décorateurs d’injection, Cli et Command serveur (Node.js)
nodefony/client RealtimeClient, le pair JSON-RPC, la cadence adaptative, et les briques isomorphes navigateur
nodefony/react NodefonyProvider et les hooks (useNodefony, useNodefonyChannel, …) navigateur (React)
nodefony/roles hasRole, RoleSet, RoleRegistry — la même évaluation de rôles des deux côtés isomorphe
nodefony/debugbar la barre de débogage embarquable dans une page navigateur
nodefony/bundler le socle de configuration du bundler, partagé par tous les paquets et par les applications outillage de build

L’import racine nodefony est conditionnel : un bundler qui cible le navigateur y trouve automatiquement le build client, là où Node.js reçoit le build serveur. C’est ce qui rend l’isomorphisme transparent — tu importes le même nom, tu obtiens la variante qui convient.

⚠️ Attention

nodefony/realtime n’existe pas. Ce sous-chemin apparaît dans plusieurs exemples au fil du dépôt, mais il n’est pas publié : l’import échoue à la résolution. Le client temps réel se prend sur nodefony/client, ou directement sur nodefony dans un contexte navigateur.

import { RealtimeClient } from "nodefony/client"; // ✅ le sous-chemin publié
import { RealtimeClient } from "nodefony/realtime"; // ❌ non résolu

Les signatures exactes ne sont jamais recopiées ici — elles divergeraient en silence. Elles vivent dans les types générés et dans le graphe symbolique du dépôt (jq '.symbols.Service' .ai/symbols.json).

⚙️ Configuration#

Le cœur ne se configure pas comme un module : il porte les valeurs d’usine de l’application entière (domaine, serveurs, journal, manifeste de modules), sur lesquelles ta configuration vient se fondre au démarrage. Deux fichiers seulement, tous deux à la racine de l’application :

La recette complète — defineConfig, use(), defineEnv, la validation, et comment un module publie ses propres clés typées — est en Configuration.

📡 Observabilité — Studio#

Le cœur n’est pas un module chargé : il n’apparaît donc pas dans la liste des modules, mais dans une carte dédiée (/nodefony/modules/core) qui rend ces pages et le graphe de ses symboles. Ce qu’il produit se lit ailleurs dans l’administration : l’écran Journaux (/nodefony/logs) montre les entrées émises par Syslog, l’écran Configuration (/nodefony/config) montre les valeurs effectives après fusion, et le suivi par identifiant de requête s’appuie sur ce que RequestContext propage.

🔗 Pour aller plus loin#