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.
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.
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.
| Process | Port | Visibility |
|---|---|---|
| Vite (React client) | 3000 | Public - the app origin |
| Nitro (h3 handlers) | 3001 | Internal - only via proxy |
| Python (FastAPI) | internal | Internal - 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 /:
1const res = await api.users.sayHello({ name: "bionicjs" });
2// -> GET /api/users/sayHello (hosted on :3000)
3// -> proxied to Nitro on :3001 internally
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.
10 / 25