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.
- Who bundles the client? Your own bundler, or a framework-chosen one you never configure?
- Who owns the server runtime? The framework’s own, or a general-purpose server framework it delegates to?
- How does a file become an endpoint? Convention, explicit route config, or both?
- 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?
| Framework | Client bundler | Server | Endpoint convention | Deploy target |
|---|---|---|---|---|
BionicJS | Vite | Nitro + h3 | server/api/*.ts | Nitro presets (not yet exposed) |
Nuxt | Vite | Nitro + h3 | server/api/*.ts | Nitro presets |
SvelteKit | Vite | Own, generated | src/routes/**/+server.ts | Adapter package |
React Router 7 | Vite | Own, generated | routes.ts config | Adapter package |
TanStack Start | Vite | Own, via plugins | createServerFn | Target plugin |
Astro | Vite | Own, minimal | src/pages/api/*.ts | Adapter package |
Next.js | Turbopack / webpack | Own runtime | app/**/route.ts | Built-in targets |
Redwood | Own (Babel, moving to Vite) | Own, serverless | api/src/functions | Deploy 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.
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.
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:
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.
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/.
| Redwood | BionicJS | Why 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/Responsesignature, 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.
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.03 / 25