Surfaces

A surface adapts a protocol to Frond operations in three steps: parse the request, call invoke, then format the response. Business logic remains in the handlers.

The framework is not the process

Fougere opens no socket. @fougere/core depends on the container, the schema and TypeScript — on no server. An operation is a function; HTTP is one projection among several, not the place it lives.

@fougere/http declares an interface, HttpRouter, and has no dependencies, not even peer ones. You build your Hono or your Fastify, hand it to createHonoRouter(app) or createFastifyRouter(app), and the surfaces program against the interface. RequestContext.request is a Web Standard Request.

What you bringWhat Fougere plugs into it
Hono, FastifycreateHonoRouter / createFastifyRouter, then REST
your GraphQL server (Apollo, Yoga…)registerAll(builder, app) returns a Pothos schema
Nuxt / Nitrothe module mounts the envelope and the REST catch-all
nothing at allfougere serve exposes the Frond over JSON-RPC, fougere call invokes it from a shell
your own loopinvoke — a call is a value, not a request

Two practical consequences. Changing HTTP server moves your routes, not your handlers. And a test calls the operation directly, without opening a port.

And the judge goes wherever JavaScript goes

The same declaration runs on both sides of the network. The validation engine (@cfworker/json-schema) was chosen to run in edge environments, and @fougere/core/contract ships as a subpath with no Node builtin in its import graph. The judge refusing a form in the browser is therefore the same object as the one refusing the body at the façade — not a rule copied faithfully, the class itself.

One caveat, because the nuance matters: it is the contract that is ubiquitous, not the app. Boot reads a Frond's sources with the TypeScript compiler API and jiti, from process.cwd() — that requires Node. What travels to a browser or a worker is the declaration, its validation and the call format.

The envelope transmits the call value directly. REST translates verbs and paths; GraphQL translates a query. These surfaces then call the same façade.

REST — mounted

The Nuxt module mounts a REST catch-all under /api/{frond}/{plural} — the plural is derived from the entity name (post → posts, category → categories):

RouteOperation
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; a named operation wins over {id}

The verb is not decorative: the routes above are the canonical table @fougere/adapter-rest derives, and this door matches against it rather than deriving a second one. A path served under another verb answers 405 with an Allow list — it never falls through to a different operation. An operation's verb follows its name (list…, find…, get… and the other read prefixes are GET, the rest are POST), and operations: { bySlug: { kind: 'query' } } in frond.config.ts states it outright.

Three properties worth knowing:

  • Same façade. A REST body uses the same validation, unknown-key rejection, and collectors as the envelope.
  • Same errors. Failures project through toHttpError: the real HTTP status from the code table, the full typed value in data.
  • List shape. list results serialize as { items, total, hasMore, endCursor }.

GraphQL — a standalone projection

@fougere/adapter-graphql derives Pothos types, inputs and CRUD operations from the same getFields() metadata every other adapter reads. It is not mounted by the Nuxt module — it ships as a projection you hand to the GraphQL server of your choice.

registerAll — the whole schema, one call

import SchemaBuilder from '@pothos/core';
import { registerAll, registerGraphQL } from '@fougere/adapter-graphql';

const builder = new SchemaBuilder({});
builder.queryType({});
builder.mutationType({});

registerAll(builder, app);          // ← every entity, every op, every relation

registerGraphQL(router, builder.toSchema());   // mount it on /graphql

registerAll walks the scanned Fronds. For each entity with a handler, it registers its type, inputs, and one field per exposed operation. ref and many relations connect the registered types; computed fields from a Presenter become resolvers.

Each generated resolver calls the handler façade and therefore keeps its validation, unknown-key rejection, and collectors. A custom resolver that calls the ORM directly must apply those rules itself. Use registerType and registerOperations for fields the projection cannot derive.

Two options, both rarely needed:

registerAll(builder, app, { surface: 'graphql' });   // honour frond.config.ts surfaces
registerAll(builder, app, { filter: (entity) => entity.name !== 'auditLog' });

demos/schema-ecommerce runs exactly this under Apollo Server on :4000.

The GraphQL projection and its demo are available. The Nuxt module does not yet mount a GraphQL endpoint automatically as it does for REST.

Writing your own

A custom surface such as RSS, webhook, or CLI follows the same sequence. The invoke page includes a complete RSS example.

Next: Deployment — what a Fougere app needs at runtime.

Built with Fougere — this site runs on the framework it documents.