Foundations/How meta-frameworks work

How meta-frameworks work

The four questions every full-stack framework answers, and who answers them how.

The question this answers

Every full-stack React framework has to answer the same four questions, and the answers are what make frameworks different from each other. Once you can answer them for one framework, the others stop being mysterious.

  1. Who bundles the client? Your own bundler, or a framework-chosen one you never configure?
  2. Who owns the server runtime? The framework’s own, or a general-purpose server framework it delegates to?
  3. How does a file become an endpoint? Convention, explicit route config, or both?
  4. How do you pick a deploy target? One command that emits the right artifact, or an adapter you install?

BionicJS answers: Vite for the client, Nitro for the server, filesystem convention for endpoints, and deployment targets inherited from Nitro’s presets. The rest of this chapter is the map of who else does what, so the choice of Nitro stops looking arbitrary.

Two families

The frameworks split along one line: do they own the server stack, or do they borrow one?

FrameworkClient bundlerServerEndpoint conventionDeploy target
BionicJSViteNitro + h3server/api/*.tsNitro presets (not yet exposed)
NuxtViteNitro + h3server/api/*.tsNitro presets
SvelteKitViteOwn, generatedsrc/routes/**/+server.tsAdapter package
React Router 7ViteOwn, generatedroutes.ts configAdapter package
TanStack StartViteOwn, via pluginscreateServerFnTarget plugin
AstroViteOwn, minimalsrc/pages/api/*.tsAdapter package
Next.jsTurbopack / webpackOwn runtimeapp/**/route.tsBuilt-in targets
RedwoodOwn (Babel, moving to Vite)Own, serverlessapi/src/functionsDeploy targets

BionicJS is not a unique idea. It is Nuxt’s server model with React’s client, plus Redwood’s directory layout. The novelty is the combination and the ergonomics, not the existence of any one piece.

Nuxt: the closest sibling

Nuxt is the framework BionicJS borrows from most directly, and the resemblance is not accidental. It also runs Vite for the client and Nitro for the server, it scans server/api/, its handlers are h3 event handlers, and it owns the Nitro configuration so you never write one.

a Nuxt endpoint
1// defineEventHandler is auto-imported from h3
2export default defineEventHandler(() => ({ hello: "world" }));

The differences are about ergonomics rather than architecture. Nuxt auto-imports a large surface, and it leans hard on Vue’s component model. BionicJS instead keeps the imports you would write by hand in plain React, and adds capabilities Nuxt has no equivalent of.

ⓘ
If you want the shortest possible answer to “how does this compare to Nuxt?”: the server half is the same architecture, and the client half is React Router instead of Vue.

Next.js: owning everything

Next.js is the counter-example: it ships its own bundler, its own server runtime, and its own deployment story. There is no Vite and no Nitro, and the trade is paid for in a large surface area and a long history of breaking changes.

Its endpoint convention is a route file exporting standard Web handlers, which is closer to Hono’s shape than to h3’s:

a Next.js route handler
1export async function GET(request: Request) {
2  return Response.json({ hello: "world" });
3}

The interesting divergence is that Next has both a Node runtime and an opt-in edge runtime, chosen per route with export const runtime = "edge". Nitro makes the equivalent choice per build, not per route - one preset for the whole server.

The Vite-based adapters

SvelteKit, React Router 7, TanStack Start, and Astro all took the same path: use Vite, generate a server, and push the runtime choice out to an adapter the user installs.

SvelteKit picks its runtime
1import adapter from "@sveltejs/adapter-node";
2export default { kit: { adapter: adapter() } };

The pattern has a real cost: the framework itself does not know how to deploy, so the deployment knowledge moves into a second package that has to be chosen, version-matched, and sometimes hand-written.

Nitro inverts this. A Nitro preset is a single option that knows how to emit for Node, Bun, Deno, Cloudflare, Vercel, Netlify, Lambda and the rest, and a framework built on Nitro inherits all of them for free. BionicJS is on the right side of that trade - but it is a promise not yet kept, because the presets are not exposed yet. See how BionicJS runs.

Redwood: the layout sibling

BionicJS’s directory structure comes from Redwood, not from Nuxt. Redwood’s insight is that a serious app has distinct top-level concerns - API functions, auth, database schema, background jobs - and that each deserves a directory rather than a directory inside src/.

RedwoodBionicJSWhy it is there
api/server/HTTP endpoints and the Node runtime
auth/server/auth/Auth provider, in the runtime that verifies it
db/server/db/Schema, migrations, and the client
jobs/jobs/Background workers, as its own process
web/app/The React client

BionicJS keeps ai/ and jobs/ at the root rather than under server/, because both are Python and both run out-of-process. That is a deliberate difference from Redwood, not an oversight.

What is worth taking from each

  • From Nuxt: own the Nitro configuration, let the user write ordinary files, inherit deployment targets for free.
  • From Redwood: a flat set of capability directories, so a project’s shape is visible at a glance.
  • From Next: the standard Web Request/Response signature, which is portable and what makes edge runtimes realistic.
  • From SvelteKit and React Router: generating the route tree instead of asking users to write a config file, and making the dev server feel like one process.
ⓘ
The pattern nobody has solved: typing across the network boundary. Every framework here either generates a client or hands you fetch. Hono’s hc<AppRouter> is the most promising idea of the lot, which is why BionicJS generates it at all - see Hono and the RPC contractfor where that currently stands.