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 bring | What Fougere plugs into it |
|---|---|
| Hono, Fastify | createHonoRouter / createFastifyRouter, then REST |
| your GraphQL server (Apollo, Yoga…) | registerAll(builder, app) returns a Pothos schema |
| Nuxt / Nitro | the module mounts the envelope and the REST catch-all |
| nothing at all | fougere serve exposes the Frond over JSON-RPC, fougere call invokes it from a shell |
| your own loop | invoke — 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):
| Route | Operation |
|---|---|
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; 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 indata. - List shape.
listresults 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.