Surfaces
Une surface adapte un protocole aux opérations d'une Frond en trois étapes : parser la requête, appeler invoke, puis formater la réponse. La logique métier reste dans les handlers.
Le framework n'est pas le process
Fougere n'ouvre aucun socket. @fougere/core dépend du container, du schéma et de
TypeScript — d'aucun serveur. Une opération est une fonction ; HTTP est une projection
parmi d'autres, pas le lieu où elle vit.
@fougere/http déclare une interface, HttpRouter, et n'a aucune dépendance, pas même
en peer. Vous construisez votre Hono ou votre Fastify, vous le passez à
createHonoRouter(app) ou createFastifyRouter(app), et les surfaces programment contre
l'interface. RequestContext.request est un Request du standard Web.
| Ce que vous amenez | Ce que Fougere y branche |
|---|---|
| Hono, Fastify | createHonoRouter / createFastifyRouter, puis REST |
| votre serveur GraphQL (Apollo, Yoga…) | registerAll(builder, app) rend un schéma Pothos |
| Nuxt / Nitro | le module monte l'enveloppe et le catch-all REST |
| rien du tout | fougere serve expose la Frond en JSON-RPC, fougere call l'appelle depuis un shell |
| votre propre boucle | invoke — un appel est une valeur, pas une requête |
Deux conséquences pratiques. Changer de serveur HTTP déplace vos routes, pas vos handlers. Et un test appelle l'opération directement, sans lever de port.
Et le juge va là où va JavaScript
La même déclaration s'exécute des deux côtés du réseau. Le moteur de validation
(@cfworker/json-schema) a été choisi pour tourner en environnement edge, et
@fougere/core/contract est publié comme sous-chemin sans aucun builtin Node dans son
graphe d'import. Le juge qui refuse un formulaire dans le navigateur est donc le même
objet que celui qui refuse le corps à la façade — pas une règle recopiée à l'identique,
la classe elle-même.
Une réserve, parce que la nuance compte : c'est le contrat qui est ubiquitaire, pas
l'app. Le boot lit les sources d'une Frond avec l'API du compilateur TypeScript et jiti,
depuis process.cwd() — cela demande Node. Ce qui voyage jusqu'au navigateur ou jusqu'à
un worker, c'est la déclaration, sa validation et le format d'appel.
L'enveloppe transmet directement la valeur d'appel. REST traduit des verbes et des chemins ; GraphQL traduit une requête. Ces surfaces appellent ensuite la même façade.
REST — montée
Le module Nuxt monte un catch-all REST sous /api/{frond}/{pluriel} — le pluriel dérive
du nom de l'entité (post → posts, category → categories) :
| Route | Opération |
|---|---|
GET /api/blog/posts | post.list |
POST /api/blog/posts | post.create |
GET /api/blog/posts/{id} | post.findById |
PUT · PATCH /api/blog/posts/{id} | post.update |
DELETE /api/blog/posts/{id} | post.delete |
POST /api/blog/posts/by-slug | post.bySlug — kebab-case → camelCase ; une opération nommée gagne sur {id} |
Le verbe n'est pas décoratif : les routes ci-dessus sont la table canonique que dérive
@fougere/adapter-rest, et cette porte s'y confronte au lieu d'en dériver une seconde. Un
chemin servi sous un autre verbe répond 405 avec la liste Allow — jamais en basculant
sur une autre opération. Le verbe d'une opération suit son nom (list…, find…, get… et
les autres préfixes de lecture sont en GET, le reste en POST), et operations: { bySlug: { kind: 'query' } } dans frond.config.ts l'énonce explicitement.
Trois propriétés à connaître :
- Même façade. Un body REST utilise la même validation, le même refus des clés inconnues et les mêmes collectors que l'enveloppe.
- Mêmes erreurs. Les échecs se projettent par
toHttpError: le vrai statut HTTP de la table des codes, la valeur typée entière dansdata. - Forme des listes. Les résultats de
listse sérialisent en{ items, total, hasMore, endCursor }.
GraphQL — une projection standalone
@fougere/adapter-graphql dérive les types, inputs et opérations CRUD Pothos depuis la
même metadata getFields() que lisent tous les autres adapters. Il n'est pas monté par
le module Nuxt — il se livre comme une projection que vous tendez au serveur GraphQL de
votre choix :
registerAll — tout le schéma, en un appel
import SchemaBuilder from '@pothos/core';
import { registerAll, registerGraphQL } from '@fougere/adapter-graphql';
const builder = new SchemaBuilder({});
builder.queryType({});
builder.mutationType({});
registerAll(builder, app); // ← chaque entité, chaque op, chaque relation
registerGraphQL(router, builder.toSchema()); // et on le monte sur /graphql
registerAll parcourt les Fronds scannées. Pour chaque entité dotée d'un handler, il
enregistre son type, ses inputs et un champ par opération exposée. Les relations ref et
many relient les types enregistrés ; les champs calculés d'un
Presenter deviennent des resolvers.
Chaque resolver généré appelle la façade du handler et conserve donc la validation, le
refus des clés inconnues et les collectors. Un resolver personnalisé qui appelle l'ORM
directement doit appliquer ces règles lui-même. Utilisez registerType et
registerOperations pour les champs que la projection ne peut pas dériver.
Deux options, rarement nécessaires :
registerAll(builder, app, { surface: 'graphql' }); // respecte les surfaces de frond.config.ts
registerAll(builder, app, { filter: (entity) => entity.name !== 'auditLog' });
demos/schema-ecommerce fait exactement ça sous Apollo Server sur :4000.
La projection GraphQL et sa démo sont disponibles. Le module Nuxt ne monte pas encore automatiquement un endpoint GraphQL comme il le fait pour REST.
Écrire la vôtre
Une surface personnalisée, par exemple RSS, webhook ou CLI, suit le même enchaînement. La page invoke contient un exemple RSS complet.
Suite : Déploiement — ce qu'une app Fougere demande au runtime.