Build and dev/The dev server

The dev server

Orchestrating engines, ports, and the API proxy.

Orchestration

When you run bionicjs dev, you are starting a master orchestrator. It spins up the Vite development server for the React frontend, the Nitro development server for the h3 backend, and (if Python features are enabled) the FastAPI/Uvicorn development server.

runDevServer
1await generateAll(cwd);
2
3const nitro = await createNitro({
4  rootDir: cwd,
5  srcDir: path.join(cwd, "server"),   // without this, /api/* 404s
6  dev: true,
7  publicAssets: existsSync(publicDir) ? [{ dir: publicDir }] : [],
8  alias: { "@bionicjs/core/server": generatedServer },
9});
10
11const nitroDevServer = createNitroDevServer(nitro);
12await nitroDevServer.listen(nitroPort);
13await prepare(nitro);
14build(nitro).catch(/* logged */);
15
16const vite = await createViteServer({
17  root: cwd,
18  server: {
19    port: publicPort,
20    strictPort: true,
21    proxy: { "/api": `http://localhost:${nitroPort}` },
22  },
23  plugins: [bionicjsRoutesPlugin(cwd), react(), tailwindcss()],
24});
25
26await vite.listen();

All of these processes run concurrently, sharing a single unified terminal output. The orchestrator handles process lifecycle - when the dev server exits, it tears down every child process cleanly.

ⓘ
The one line that is easy to get wrong is srcDir. Nitro scans <srcDir>/api, not <srcDir>/server/api, so without it Nitro looks for ./api, registers zero routes, and every endpoint 404s with no error at all. Nitro and h3 covers the full story, including the publicAssets side effect and what changes in Nitro v3.

Ports

The public origin is localhost:3000. BionicJS uses strictPort, so port conflicts are visible immediately rather than silently moving the app to a different port.

ProcessPortVisibility
Vite (React client)3000Public - the app origin
Nitro (h3 handlers)3001Internal - only via proxy
Python (FastAPI)internalInternal - only via Nitro RPC

Because Nitro runs on an internal port, the developer never talks to it directly. JavaScript must be loaded by Vite to benefit from HMR and module resolution; Nitro handles HTTP directly and can run on any internal port without affecting the developer experience.

Proxying

To avoid CORS issues and simplify frontend requests, the Vite server is configured to proxy all /api requests directly to the Nitro server. The frontend simply fetches from /:

app/page.tsx
1const res = await api.users.sayHello({ name: "bionicjs" });
2// -> GET /api/users/sayHello (hosted on :3000)
3// -> proxied to Nitro on :3001 internally
ⓘ
Browsers enforce same-origin for cookies and authorization headers. By proxying at the Vite layer, BionicJS keeps everything on one origin - no CORS configuration, no withCredentials hacks, and cookies from auth providers flow naturally.

In production there is no Vite and no proxy. The single Nitro server serves both the static client assets and the API. The application code never needs to know which environment it is running in.