Pick a host
Your fronds run under whichever framework serves your pages — the host: Nuxt, Next, TanStack
Start, React Router, SvelteKit, Express, or any engine that speaks Request. A Frond does not know which one it runs under, and nothing in fronds/ imports
one — the five demos serving the same blog under Next, TanStack Start, React Router,
SvelteKit and Express share a fronds/ directory that is identical byte for byte.
(demos/nuxt-blog is the flagship and carries more: presenters, collectors, a named
surface.)
What a host does is hand its requests to one function and, if it renders pages, offer the four
client primitives. Everything else — the boot, the validator, the REST table, the form contract —
lives in @fougere/app and is the same everywhere.
What each host costs
| Host | Package | What you write |
|---|---|---|
| Nuxt | @fougere/nuxt | one line in nuxt.config |
| Next | @fougere/next | two one-line route files + withFougere() |
| TanStack Start | none | two route files + the Vite plugin |
| React Router | none | three resource routes + the Vite plugin |
| SvelteKit | none | two +server.ts + the Vite plugin |
| Express | none | app.use(fougere()) |
| Hono, a Worker, any engine | none | one line — answer |
fougere new writes these lines into the shell the host's own tool made, so a new app starts from
what create-nuxt, create-next-app, create-vite or sv create writes today — see
fougere new. A host finds the fronds on its own — the nearest directory, up from the app, that holds
fronds/ — so root is written only when they sit elsewhere, the same option for every host.
Three hosts need no package of their own, and that is not an accident: they serve
Web-standard Request/Response, so the facades go in unchanged. @fougere/next exists
because Next has next/headers — one import — and Nuxt's module is larger because an h3
event is not a Request.
What an app publishes
Which protocols are served is a fact about the app, declared once beside sources: and
fronds::
// fougere.config.ts
import '@fougere/adapter-rest';
import '@fougere/adapter-graphql';
export default defineFougere({
protocols: { rest: {}, graphql: {} },
});
graphql: {} is the whole GraphQL setup: the schema is built from the entities the
app already scanned, so there is no builder to construct and no types to register. The import
makes the protocol available and the key serves it — an app that imports neither carries no
schema builder, and naming one it did not import refuses the boot, naming the import.
Absent means not served. The facade may be mounted — the route file exists, the
middleware is installed — and it will still decline, because the host does not get to
make this decision. answer reads the declaration itself, so every host inherits one answer
instead of each spelling it their own way.
The call envelope is not listed there: it is the wire the client primitives use, not a projection an app chooses to publish.
Mounting the facades
Mounting a facade is not the same as publishing an adapter: what you write below puts
the facade in place, and protocols: decides whether it serves anything.
A Web-standard host — Next, TanStack Start, React Router, SvelteKit — mounts the handlers directly:
// SvelteKit — src/routes/_fougere/call/+server.ts
import { fougere } from '@fougere/app/web';
export const POST = ({ request }: { request: Request }) => fougere(request);
fougere takes a Request and returns a Response, and reads the door off the path, so every
route file hands it the same function. The file you create is the consent: no file, no facade.
Express adds them as one middleware, like express.json() or cors():
import { fougere } from '@fougere/app/express';
app.use(fougere()); // every door; what each one serves is `protocols:` in fougere.config.ts
A path Fougere does not serve calls next(), so an app's own routes keep answering
whether they were registered before or after.
Nuxt mounts them itself — its module has an API for that, and adds the composables at the same time.
Any engine
Every host above ends in the same function. When you already hold the app, call it yourself:
import { answer } from '@fougere/app/answer';
hono.all('*', async (c) => (await answer(app, c.req.raw)) ?? c.notFound());
answer(app, request) reads the door off the path — /_fougere/call, REST under its prefix,
/graphql (GraphQL over GET too, a mutation refused there) — and answers undefined when none
of them is the request's, so the engine's own routes keep answering. It loads no boot, which is
what lets a Worker import it (demos/cloudflare-d1). There is no router of ours to adapt: the
engine is one, and its middleware is how you stand around the app — leaving a route out is a
middleware answering before answer does. fougere(request) from @fougere/app/web is the same
function for a host that has only the request: it boots the app at the first one.
fougere serve answers this way too, beside the envelope it serves its peers, and server: { use }
in the config stands around that half (config).
The envelope is read off the Node stream rather than through a Request: it is the wire between two
processes, and the same call answered 29 % less the other way.
The client primitives
useQuery, useCommand, useFormFor and useCurrentUser exist per UI framework, not
per host:
@fougere/react— React, whatever renders it@fougere/svelte— Svelte, as stores@fougere/nuxt— Vue, through Nuxt's own data layer
The rules they obey are shared: a call names a facade and an operation, and a command on a facade revalidates every mounted query on that facade. Only the state model differs.
The one thing every bundler must be told
A call through a facade names its address as a literal ({ address: 'post' }), so nothing a
minifier renames reaches it. What still reads a class name is the form — useFormFor(Post)
submits to the address named after the entity — and invoke(Post, 'list') on the server. A
minifier renames classes, so a production build would designate an entity nobody hosts:
Entity 'j' is not hosted here. Hosted here: post.
You do not have to declare anything for this. @fougere/nuxt and @fougere/vite reserve
the entity names for you — the first from the scan it just ran, the second by reading
fronds/<frond>/entities/. On Next, withFougere() does the same through webpack:
// next.config.ts
import { withFougere } from '@fougere/next/config';
export default withFougere(nextConfig);
// vite.config.ts — TanStack Start, React Router, SvelteKit
import { fougere } from '@fougere/vite';
export default defineConfig({ plugins: [fougere(), sveltekit()] });
Express needs none of this: it runs your sources, nothing is minified.
Two hosts at once
Nothing stops a frond from being served by more than one host: run it in its own process
with serve() and let the others reach it through fronds:. That is the
gradient, and the host is not part of the decision.