Philosophie
Fougere décrit une application avec des données, des opérations, des contrats et des frontières de domaine. Des adapters relient ensuite ce modèle à Nuxt, HTTP, SQL, au cache ou à un processus distant.
Les déclarations se projettent
Une entité décrit des données indépendamment de leur stockage. Sa déclaration fournit le type TypeScript, la validation et les métadonnées utilisées par les formulaires, SQLite et GraphQL. Lorsqu'une même règle apparaît dans plusieurs couches, elle doit être placée dans la déclaration commune si son périmètre le permet.
Des opérations, pas des écritures de champ
Une Frond expose des commands pour modifier le système, des queries pour le lire
et, lorsque c'est utile, des events produits par les commands. Crud(Post) fournit
les cinq opérations standard. Une transition métier utilise un verbe explicite :
publish vérifie ses règles avant de modifier status, au
lieu de laisser le client écrire ce champ directement.
La Frond possède
Une Frond regroupe les entités, opérations, règles d'accès et adapters d'un domaine. Les interactions avec une autre Frond passent par ses contrats publics. L'application compose ces modules sans déplacer leur logique dans les pages, les fetchs ou les stores.
La topologie est de la configuration
Le fichier de configuration indique si une Frond s'exécute localement ou sur un hôte distant. Les appels utilisent la même API dans les deux cas. Le runner exécute un appel local en mémoire et utilise le transport configuré pour un appel distant.
Cette frontière permet aussi à une Frond d'avoir son propre dépôt et son propre cycle de
publication : seul son contrat doit voyager, jamais son code source — fougere sync
le lit sur l'hôte qui tourne, fougere build-frond en fait un paquet installable
(où le code vit).
HTTP est une projection, pas un socle
Beaucoup de frameworks partent d'une route : l'objet premier est une requête, et le modèle, la validation et la couche métier viennent s'y accrocher. Ici l'objet premier est l'opération — une méthode publique qui reçoit une invocation, et qui n'a jamais entendu parler de HTTP.
Ce n'est pas une préférence de style, c'est ce qui rend le reste possible. Une opération qui
ignore le transport s'appelle en mémoire, se met sur un fil JSON-RPC, se traduit en route
REST, en champ GraphQL ou en commande de shell — sans qu'aucune de ces formes ne soit
privilégiée. @fougere/core ne dépend d'aucun serveur, et
@fougere/http n'a aucune
dépendance du tout : vous apportez Hono, Fastify, Nitro — ou rien.
Le corollaire est que JavaScript vous emmène partout où il va. La déclaration et son juge tournent dans le navigateur comme à la façade : le même objet, pas une copie fidèle.
Le transport est une feuille
Une application Fougere complète peut n'avoir aucun HTTP. Les entités, les opérations, le stockage, la validation, l'injection et les seeds fonctionnent sans qu'aucun serveur ne démarre. Une commande de shell, un worker, un test, un appel en mémoire ne sont pas des versions dégradées de l'application : ce sont des usages entiers.
C'est pour cela que HTTP figure au même rang que les autres projections, et non en dessous. Une même déclaration donne la table SQL, le type GraphQL, le contrat de formulaire et la route : quatre lectures d'un seul énoncé, aucune n'étant le sol des autres.
Deux conséquences, et ce sont elles qui rendent le principe utile :
- si le transport était la fondation, on ne pourrait pas l'enlever — donc pas d'appel en mémoire, donc pas de gradient ;
- les surfaces ne pourraient pas rendre le même verdict, puisque l'une d'elles servirait de sol aux autres.
Ce n'est donc pas une revendication d'indépendance vis-à-vis d'un serveur, ni un choix d'implémentation. C'est la règle du haut de cette page — une déclaration, des projections — appliquée à l'endroit où beaucoup de frameworks placent leur socle.
Un état est perdable ou durable
Pour choisir entre cache et stockage durable, demandez ce que la perte de la donnée
change. Une valeur recalculable à partir d'une query peut rester dans un cache. Une valeur
dont la perte modifie le comportement doit être stockée comme donnée métier, même si sa
durée de vie est courte. Un code OAuth à usage unique peut par exemple être représenté
par { code, userId, expiresAt } avec une lecture destructrice.
Embarqué ou externalisé
Les éléments utilisés dans plusieurs contextes sont livrés avec la classe ; les détails d'exécution sont branchés par le runtime.
- Embarqué — la validation, les projections d'entrée ou de sortie et les métadonnées. Ces éléments peuvent être utilisés dans le navigateur, à la façade ou dans une Frond distante.
- Externalisé — le stockage, le transport et le rendu. Ces adapters peuvent varier selon l'application et la surface utilisée.
La validation est embarquée pour appliquer les mêmes règles dans chaque contexte. Le stockage reste externe afin que l'application puisse choisir son adapter. Les métadonnées ne dépendent pas de la topologie ; leur utilisation dépend du runtime qui les charge.
L'ordre de conception
Quand une décision de design se présente, poser les questions dans cet ordre :
- Quel est le concept applicatif ?
- Quelle est sa frontière de propriété ?
- Quels contrats doit-il exposer ?
- Quelles opérations changent l'état ?
- Quelles queries le lisent ?
- Qu'est-ce qui invalide quoi ?
- Quel adapter exécute ou projette ce modèle ?
Cet ordre évite de répartir une règle entre une route, une page, une table et un transport.
Pourquoi « Fougere »
Une fougère est composée de frondes qui répètent une structure proche de la plante. Le nom fait référence à la composition du framework : des champs forment une entité, les entités et leurs opérations forment une Frond, puis l'application compose plusieurs Fronds.
Position
Fougere vise les applications data-intensive qui utilisent un même modèle métier depuis plusieurs surfaces et modes d'exécution. Nuxt, le CRUD et les transports sont des intégrations de ce modèle, pas sa définition.
Suite : La Frond.