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 amenezCe que Fougere y branche
Hono, FastifycreateHonoRouter / createFastifyRouter, puis REST
votre serveur GraphQL (Apollo, Yoga…)registerAll(builder, app) rend un schéma Pothos
Nuxt / Nitrole module monte l'enveloppe et le catch-all REST
rien du toutfougere serve expose la Frond en JSON-RPC, fougere call l'appelle depuis un shell
votre propre boucleinvoke — 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) :

RouteOpération
GET /api/blog/postspost.list
POST /api/blog/postspost.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-slugpost.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 dans data.
  • Forme des listes. Les résultats de list se 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.

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