Web

Build web applications with Bini.js — the default platform target.

Web Overview

Web is the default platform target in Bini.js. It's a standard Vite + React SPA with file-based routing, pre-rendering support, and a Hono API layer. Your application runs in the browser and can be deployed to any hosting platform.

SPA

Single-page application with client-side routing

API Layer

Hono-powered API routes in src/app/api/

Pre-rendering

Static HTML with bini-ssg

Windows

Create a web application on Windows using the CLI. Web is the default platform, so you don't need to specify it.

Interactive Mode

Run the CLI and select web when prompted:

npx create-bini-app@latest

Prompt: Which platform would you like to target?

web / windows / macos / linux / android / ios

Select web and press Enter

With --platform Flag

npx create-bini-app@latest my-app --platform web

Default (No Flag)

npx create-bini-app@latest my-app
Web is the default platform on Windows. All commands work the same way as on other operating systems.

macOS

Create a web application on macOS using the CLI. Web is the default platform, so you don't need to specify it.

Interactive Mode

Run the CLI and select web when prompted:

npx create-bini-app@latest

Prompt: Which platform would you like to target?

web / windows / macos / linux / android / ios

Select web and press Enter

With --platform Flag

npx create-bini-app@latest my-app --platform web

Default (No Flag)

npx create-bini-app@latest my-app
Web is the default platform on macOS. All commands work the same way as on other operating systems.

Linux

Create a web application on Linux using the CLI. Web is the default platform, so you don't need to specify it.

Interactive Mode

Run the CLI and select web when prompted:

npx create-bini-app@latest

Prompt: Which platform would you like to target?

web / windows / macos / linux / android / ios

Select web and press Enter

With --platform Flag

npx create-bini-app@latest my-app --platform web

Default (No Flag)

npx create-bini-app@latest my-app
Web is the default platform on Linux. All commands work the same way as on other operating systems.

Development Server

Start the development server with HMR (Hot Module Replacement):

npm run dev

The dev server provides:

  • Fast refresh with HMR
  • File-based routing with live updates
  • API routes served at /api/*
  • Environment variables from .env files
  • Error overlay with bini-overlay

Production Server

Build and serve your application in production mode:

npm run build
npm start

bini-server is a zero-dependency production server that includes:

  • Static file serving with ETag/304 caching
  • API routes from src/app/api/
  • SPA fallback for client-side routing
  • Graceful shutdown
  • Configurable timeouts and body limits

Pre-rendering

Every route is pre-rendered to static HTML during npm run build. There is no separate export command or export mode — bini-ssg drives pre-rendering as part of the same build.

How It Works

npm run build type-checks (TypeScript projects) and then runs vite build. The bini-ssg plugin drives pre-rendering as part of that same build:

  • Static routes (e.g., /, /about) are rendered to real server-rendered HTML
  • Dynamic routes (e.g., /blog/:slug) get a shell page with hydration
  • React 19 renderToPipeableStream is used for server rendering
  • StaticRouter from React Router provides the routing context

Your render() Function

The render() function is exported from src/main.tsx:

src/main.tsx
// src/main.tsx
import { createRoot } from 'react-dom/client'
import App from './App'

// Client mount
createRoot(document.getElementById('root')!).render(<App />)

// SSG render (called by bini-ssg, Node-only)
export async function render(url: string): Promise<string> {
  const { renderToString } = await import('react-dom/server')
  const { StaticRouter } = await import('react-router-dom/server')
  const { AppRoutes } = await import('./App')

  return renderToString(
    <StaticRouter location={url}>
      <AppRoutes />
    </StaticRouter>
  )
}

Build Command

npm run build

The output is real server-rendered markup, not a client-only shell. The client then hydrates it with hydrateRoot on load.

Output Structure

dist/ ├── index.html ← Pre-rendered '/' ├── about/ │ └── index.html ← Pre-rendered '/about' ├── blog/ │ └── [slug]/ │ └── index.html ← Shell page for '/blog/:slug' ├── docs/ │ └── [...slug]/ │ └── index.html ← Shell page for '/docs/*' ├── js/ ← Your compiled JavaScript files │ └── index-[hash].js └── css/ ← Your compiled CSS files └── index-[hash].css

Hydration and Shell Pages

For dynamic routes, bini-ssg injects a marker script:

<script>window.__BINI_SHELL__=true;</script>

Your client entry checks this flag:

// src/main.tsx
const root = document.getElementById('root')!

if (window.__BINI_SHELL__) {
  createRoot(root).render(<App />)
} else {
  hydrateRoot(root, <App />)
}

Deployment

bini-deploy is bundled into every scaffold and exposed as npm run deploy.

Deploy Command

npm run deploy

For web, npm run deploy prompts for a hosting target and generates the appropriate configuration:

PlatformRuntimeFile Generated
Node.jsNode.js— (bini-server reads src/app/api/ directly)
NetlifyEdge Functions (Deno)netlify/edge-functions/api.ts + netlify.toml
VercelEdge Runtimeapi/index.ts + vercel.json
CloudflareWorkersworker.ts + wrangler.toml
DenoDenoserver/index.ts

Deployment Options

  • SPA + API Server: Build with npm run build, deploy with npm start (requires Node.js)
  • Pre-rendered Static: Build with npm run build, deploy the dist/ folder to any static hosting
  • Edge/Serverless: Use npm run deploy to generate platform-specific entry files

Node.js Deployment

For Node.js hosts (Railway, Render, Fly.io, a VPS):

npm run build && npm start

bini-server reads handlers directly from src/app/api/, so deploy the whole project — not just dist/. Use pm2 on a bare VPS.

GitHub Pages / Subpaths

Set base: '/my-repo/' in vite.config.ts, then npm run build for a fully pre-rendered, subpath-aware dist/.

vite.config.ts
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  base: '/my-repo/',  // GitHub Pages subpath
})