Qu'est-ce que Fougere

Fougere est un framework TypeScript construit sur une seule idée : vous déclarez le domaine, et tout en dérive — jusqu'au process où il tourne.

Énoncée à la forme négative, elle se vérifie : la déclaration ne nomme rien en dehors d'elle-même. Ni table, ni protocole, ni hôte, ni adresse n'y figurent. Deux conséquences en découlent, et on les vend d'habitude comme deux fonctionnalités distinctes :

La déclaration ne nomme pas…donc cette chose est…son nom usuel
sa table, son type GraphQL, son formulaire, son jugedérivée d'ellesingle-schema
son hôte, son stockage, sa porte, son adressechoisie en dehors d'ellele gradient

Une seule règle lue dans deux directions — ce qu'une déclaration produit, et ce dont on peut l'entourer. Le reste de cette page, ce sont ces deux lectures.

Ce qui en dérive

Single-schema. Une classe d'entité déclare vos données une fois — et juge elle-même ses entrées : le même validate() tourne dans le navigateur et à la façade. Le juge est lui-même une projection — dérivée de l'axe shape — mais une projection normative, embarquée avec la classe : toutes les autres doivent lui obéir, et elle ne peut pas dériver seule. Les tables SQLite, les types GraphQL, les contrats de formulaire et les surfaces d'API sont des projections de cette déclaration — rien n'est écrit deux fois.

import { entity, primary, text, created, oneOf, date, readOnly, optional } from '@fougere/schema';

export default class Post extends entity({
  id: primary(),
  title: text({ min: 1, max: 160 }),
  body: optional(text()),
  createdAt: created(),
  status: readOnly(oneOf('draft', 'published', { default: 'draft' })),
  publishedAt: readOnly(optional(date())),
}) {}

Le schéma — déclaré une fois

class Post extends entity({
  id: primary(),
  title: text({ min: 1 }),
  status: readOnly(oneOf(
    'draft', 'published')),
}) {}

Post.validate(input)dérivée du shape · embarquée avec la classe

Surface d'API

post.list · post.publish

Table de base de données

auto-DDL → SQLite

Type TypeScript

function render(p: Post)

Contrat de formulaire

useFormFor(Post)

Type GraphQL

type Post { … }

Désignation & DI

useQuery(Post, 'list')

Un noyau, six projections — changez la déclaration, chaque projection suit.

Cette seule classe est à la fois :

  • le type TypeScript d'une ligne (function render(p: Post) — pas d'Infer<typeof …>),
  • le validateur des entrées client (Post.validate(input)),
  • la metadata que chaque adapter lit (Post.getFields()),
  • la désignation que les pages utilisent pour appeler les opérations (useQuery(Post, 'list')),
  • le type nominal que l'injection de dépendances matche dans les signatures (user?: User).

Ce qui est choisi en dehors

Le gradient. La logique métier vit dans des Fronds — des modules autonomes d'entités, handlers, collectors et seeds. Une Frond tourne in-process aujourd'hui et dans son propre process demain, derrière JSON-RPC 2.0, avec un code utilisateur identique. L'énoncé de topologie entier tient en une ligne de config :

// fougere.config.ts
remotes: { blog: 'http://127.0.0.1:4100' }

Pas de RPC sans voyage : un appel est une valeur (entity, operation, invocation) ; le runner l'exécute directement en mémoire quand la Frond est locale et la met sur le fil quand elle est distante. Les transports déplacent la valeur — ils ne la remodèlent jamais.

Donc le split coûte le saut et le JSON qui voyage avec, et rien d'autre : aucune sérialisation que le chemin local éviterait, aucun impôt du framework par-dessus le réseau.

Les quatre familles que la règle refuse de nommer, dessinées — le gradient étant la quatrième, lue comme un mouvement plutôt que comme une liste :

La Frond — ce que vous avez écrit

entities · operations · its judge · its facts

elle n'en nomme aucune des quatre

L'hôte

Nuxt · Next · SvelteKit

TanStack · React Router · Express · none

Le stockage

SQLite · Postgres

MySQL · SQL Server

La porte

in memory · JSON-RPC

REST · GraphQL

Le lieu

same process · another process

another repo · another language

lue comme un mouvement, celle-ci est le gradient

La Frond ne nomme aucune des quatre familles. Chacune est choisie en dehors d'elle — et la dernière, lue comme un mouvement plutôt que comme une liste, est le gradient.

Et ça se vérifie au diff : les cinq demos qui servent ce même blog sous Next, TanStack Start, React Router, SvelteKit et Express partagent un répertoire fronds/ identique à l'octet, et trois de ces hôtes ne demandent aucun paquet Fougere. Alors progressif ne veut dire que ceci : chaque pas vers l'extérieur énonce son prix, et aucun ne demande de réécrire ce que vous avez écrit.

Ordre de lecture

ConceptsPhilosophie · La Frond · Le socle

Côté serveurUne app que vous avez déjà · Apportez votre schéma · Démarrer · La CLI · Entités · Vues · Standard Schema · Handlers · Presenters · Collectors · Erreurs · Seeds · Les faits · Le port Storage · Les repositories

Côté clientQueries & commands · Formulaires · Session · invoke

TopologieLe gradient · Surfaces · Déploiement · Les hôtes · Les sources

Statut. Fougere est en alpha : les paquets @fougere/* sont sur npm sous le tag alpha, et cette documentation décrit l'API telle qu'elle existe dans le dépôt aujourd'hui. Ce site tourne dessus.

Construit avec Fougere — ce site tourne sur le framework qu'il documente.