Production server for bini-router apps.
Zero-dependency, secure-by-default, production-grade server for your static sites and API routes.
- 🗂️ Static file serving — Streams
dist/with proper MIME types, ETag, and cache headers - 🌐 API routes — Serves
/api/*fromsrc/app/api/(Hono apps + plain functions) - 🔀 SPA fallback — Unknown routes serve
dist/index.htmlautomatically - 🏷️ ETag support —
304 Not Modifiedresponses for unchanged static files - ⚡ Lazy route loading — API routes scanned only on first request for fast startup
- 🛡️ CORS — Enabled by default, configurable via
CORS_ENABLED(supportsBINI_*,VITE_*, no prefix) - 🔒 Body limits — Configurable request body size limit (default 10MB)
- ⏱️ Timeouts — Configurable body read + handler timeouts (default 30s each)
- 🚫 Path traversal protection — Guards against
..and//in URLs - 💾 Module cache — Caches imported handlers with mtime invalidation
- 🔌 Port auto-increment — Starts at
3000, auto-increments if busy
- 🌿 Auto env loading —
.envfiles detected and listed at startup - ⌨️ Interactive shortcuts —
hfor help,oto open browser,qto quit - 🖥️ Cross-platform — Works on Windows, macOS, and Linux
- 🪄 Graceful shutdown — Handles
SIGTERM+SIGINTwith timeout fallback - 📦 Zero dependencies — Only uses Node.js built-in modules
- 🔧 Flexible config — Supports
BINI_*,VITE_*, or no prefix env vars
- Node.js ≥ 20.19.0
- A bini-router project with a built
dist/ - API handlers in
src/app/api/(if using API routes)
npm install bini-server
# or
pnpm add bini-server
# or
yarn add bini-server{
"scripts": {
"build": "vite build",
"start": "bini-server"
}
}npm run build # Build your app
npm start # Serve in production ß Bini.js (production)
➜ Environments: .env, .env.local
➜ Local: http://localhost:3000/
➜ Network: http://192.168.1.5:3000/
➜ press h + enter to show help
While the server is running, type a key and press enter:
| Key | Action |
|---|---|
h |
Show available shortcuts |
o |
Open your app in the default browser |
q |
Quit the server |
Keyboard shortcuts are automatically disabled in non-interactive environments (like Render, CI/CD).
At startup, bini-server automatically detects and loads:
.env.local.env.[NODE_ENV].local(e.g.,.env.production.local).env.[NODE_ENV](e.g.,.env.production).env
All detected files are listed in the startup banner.
All environment variables support three naming conventions:
| Convention | Example | Priority |
|---|---|---|
BINI_* |
BINI_PORT=3000 |
Highest |
VITE_* |
VITE_PORT=3000 |
Medium |
| No prefix | PORT=3000 |
Lowest |
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP port to listen on |
CORS_ENABLED |
true |
Enable/disable CORS on API routes |
API_DIR |
src/app/api |
Path to API handlers directory |
DIST_DIR |
dist |
Path to static files directory |
BODY_TIMEOUT_SECS |
30 |
Max seconds to read request body |
HANDLER_TIMEOUT_SECS |
30 |
Max seconds for handler to respond |
BODY_SIZE_LIMIT |
10485760 |
Max request body size in bytes (10MB) |
# .env file
PORT=8080
CORS_ENABLED=false
API_DIR=src/api
BODY_SIZE_LIMIT=5242880 # 5MB
# Or inline
PORT=3001 BINI_CORS_ENABLED=false bini-server
# Or with VITE prefix
VITE_PORT=3000 VITE_CORS_ENABLED=false bini-servermy-app/
├── dist/ # Built static files (required)
│ ├── index.html
│ ├── assets/
│ └── ...
├── src/
│ ├── app/
│ │ ├── api/ # API handlers (optional)
│ │ │ ├── users.ts
│ │ │ └── posts/
│ │ │ ├── index.ts
│ │ │ └── [id].ts
│ │ └── layout.tsx
│ └── main.tsx
├── .env # Environment variables
├── package.json
└── vite.config.ts
// 1. Hono App (Recommended)
import { Hono } from 'hono';
const app = new Hono();
app.get('/users', (c) => c.json({ users: [] }));
export default app;
// 2. Plain Function
export default (req: Request) => {
return Response.json({ message: 'Hello' });
};Only .ts and .js files are supported for API routes (Next.js convention).
src/app/api/
users/
[id].ts → /api/users/:id
posts/
[...slug].ts → /api/posts/*
// src/app/api/users/[id].ts
export default (req: Request) => {
const params = JSON.parse(req.headers.get('x-bini-params') || '{}');
// params.id → '123'
return Response.json({ id: params.id });
};CORS is enabled by default with these headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD
Access-Control-Allow-Headers: Content-Type,Authorization,X-Request-ID
Disable with CORS_ENABLED=false, BINI_CORS_ENABLED=false, or VITE_CORS_ENABLED=false.
All common file types are served with correct MIME types:
- HTML, CSS, JavaScript, JSON
- Images: PNG, JPEG, GIF, SVG, WebP, AVIF, ICO
- Fonts: WOFF, WOFF2, TTF, EOT
- Documents: TXT, XML
- Web manifests
| File Type | Cache Policy |
|---|---|
Assets (/assets/*) |
public, max-age=31536000, immutable (1 year) |
| All other files | no-cache |
Automatically generates ETags from file size + mtimeMs:
- Sends
ETagheader on first request - Handles
If-None-Matchfor304 Not Modifiedresponses - Uses MD5 hash (16 chars) for efficient caching
bini-server runs API handlers directly from src/app/api/ — they are not compiled into dist/. When deploying, ensure your server has access to both dist/ and src/app/api/.
- ✅ VPS/pm2: Deploy the full project directory
- ✅ Railway/Render/Fly.io: Automatic (clones your repository)
- ✅ Docker: Copy both
dist/andsrc/directories
npm run build
npm start
# With pm2 (recommended)
npm install -g pm2
pm2 start "npm start" --name my-app
pm2 save
pm2 startup| Platform | Start Command | Notes |
|---|---|---|
| Railway | npm start |
PORT injected automatically |
| Render | npm start |
PORT injected automatically |
| Fly.io | npm start |
See fly.toml example below |
| Heroku | npm start |
PORT injected automatically |
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]# fly.toml
[processes]
app = "npm start"| Feature | vite preview |
bini-server |
|---|---|---|
Serves dist/ |
✅ | ✅ |
| API routes | ✅ | ✅ |
| SPA fallback | ✅ | ✅ |
| Auto env loading | ✅ | ✅ |
| ETag / 304 support | ❌ | ✅ |
| Body timeout | ❌ | ✅ (30s) |
| Body size limit | ❌ | ✅ (10MB) |
| Handler timeout | ❌ | ✅ (30s) |
| Graceful shutdown | ❌ | ✅ |
| Module cache | ❌ | ✅ |
| Configurable dirs | ❌ | ✅ |
| CORS control | ❌ | ✅ |
| Zero dependencies | ❌ | ✅ |
| Production use | ✅ Production-ready |
| Feature | Default | Configurable |
|---|---|---|
| CORS | Enabled | ✅ via CORS_ENABLED |
| Body size limit | 10MB | ✅ via BODY_SIZE_LIMIT |
| Request timeout | 30s | ✅ via BODY_TIMEOUT_SECS |
| Handler timeout | 30s | ✅ via HANDLER_TIMEOUT_SECS |
| Path traversal | Blocked | ✅ (guard in place) |
# Check static files
curl http://localhost:3000/
# Check API routes
curl http://localhost:3000/api/hello
# Check ETag
curl -I http://localhost:3000/styles.css
# Test 304 Not Modified
curl -I http://localhost:3000/styles.css \
-H "If-None-Match: [etag_from_previous_request]"
# Test CORS
curl -X OPTIONS http://localhost:3000/api/hello \
-H "Origin: http://example.com"CORS_ENABLED=true
BODY_TIMEOUT_SECS=0
HANDLER_TIMEOUT_SECS=0
BODY_SIZE_LIMIT=0
NODE_ENV=developmentCORS_ENABLED=true
BODY_TIMEOUT_SECS=30
HANDLER_TIMEOUT_SECS=30
BODY_SIZE_LIMIT=10485760
NODE_ENV=productionCORS_ENABLED=false
BODY_SIZE_LIMIT=5242880 # 5MBCORS_ENABLED=true
BODY_SIZE_LIMIT=1073741824 # 1GB
BODY_TIMEOUT_SECS=300 # 5 minutesBINI_*(highest)VITE_*(medium)- No prefix (lowest)
| Code | Description |
|---|---|
200 |
Success |
204 |
OPTIONS preflight success |
304 |
Not Modified (ETag match) |
400 |
Bad Request URL |
404 |
Route not found |
408 |
Request timeout |
413 |
Payload too large |
500 |
Internal server error |
GET,POST,PUT,PATCH,DELETEOPTIONS(CORS preflight)HEAD(with ETag support)
- Fork the repository
- Create your feature branch
- Commit your changes
- Push to the branch
- Open a Pull Request
MIT © Binidu Ranasinghe
Built with ❤️ by Binidu Ranasinghe