Node.js, Bun, and Deno
Set target in void.json to build for Node.js, Bun, or Deno. You can run the resulting server on your own machine, in a container, or with a hosting provider.
{ "target": "node" }Supported Targets
| Target | Server | Static Assets |
|---|---|---|
"node" | @hono/node-server | @hono/node-server/serve-static |
"bun" | Bun.serve() | hono/bun |
"deno" | Deno.serve() | hono/deno |
All three produce the same project structure and use the same Hono-based routing. The only difference is the HTTP server and static file middleware.
Getting Started
1. Configure the target
// void.json
{ "target": "node" }2. Install the server dependency (Node.js only)
Node.js requires @hono/node-server as an additional dependency. Bun and Deno use their built-in HTTP servers.
npm install @hono/node-server3. Develop
npx vite devThe dev server runs your Hono routes in Node.js through Vite's SSR module loading. You get the same hot-reload experience as the Cloudflare target, but without workerd.
4. Build and run
npx vite build
node dist/ssr/index.jsFor Bun:
bunx vite build
bun dist/ssr/index.jsFor Deno:
deno run -A npm:vite build
deno run -A dist/ssr/index.jsRun these commands from your app directory. The server listens on PORT (env variable) or 3000 by default.
Pages builds load their client manifest from dist/client/.vite/manifest.json and serve the generated JavaScript, CSS, and public assets. Keep dist/ssr and dist/client together when deploying. Asset paths resolve from the emitted server module, so the app also works when imported or started from another working directory.
Build Output
The build produces two entry points:
dist/
ssr/
app.js ← Hono app with static asset middleware (default export)
index.js ← imports app.js and starts the HTTP server
client/ ← static assets (pages mode only)
.vite/manifest.json
assets/
...app.js: exports the Hono app instance. Use it for programmatic embedding, testing, or custom server setups.index.js: importsapp.jsand starts the server. This is what you run in production.
vite preview also uses app.js to serve your built app locally.
What Works
These Void features work identically across all targets:
- File-based routing (
routes/,middleware/) - Pages mode with SSR (React, Vue, Svelte, Solid)
- Typed fetch client (
void/client) - Custom headers (
routing.headers) - Redirects (
routing.redirects) - Environment variables (
.envfiles) - Static site generation
- Vite preview for testing production builds locally
What's Different
No Cloudflare bindings
The following imports are not available with a non-CF target and produce a compile-time error:
| Import | CF Feature |
|---|---|
void/db | D1 (SQL database) |
void/kv | KV (key-value storage) |
void/storage | R2 (blob storage) |
void/auth | Void-managed Better Auth |
void/ai | Workers AI |
void/env | CF env type augmentation |
If you need a database or storage, use an external provider and connect via standard Node.js libraries.
Void-managed WebSocket route files (*.ws.ts) are still Cloudflare-only because they compile to Durable Objects. The void/ws subpath is not compile-time blocked on non-CF targets, so browser-side connect() code can still be bundled when a WebSocket runtime is available.
No cron job runtime
You can still define cron jobs in crons/ and they will compile into the bundle, but there is no built-in scheduler to invoke them. On Cloudflare, Workers Cron Triggers call the scheduled handler automatically. On Node.js, you'll need an external scheduler (e.g. node-cron, systemd timers, or your hosting platform's cron) to trigger the exported handler.
No inbound email
Inbound email handlers in email/ need Cloudflare Email Routing to invoke the worker's email() export, and the Node.js entry exposes HTTP only. An email/ directory fails the build with guidance. Outbound sendEmail has no send transport on these targets either: it returns a BINDING_MISSING error.
No prerendering
Prerendering is a platform feature that relies on Cloudflare's edge cache and the Void deploy pipeline. It is not available for non-CF targets.
No void deploy
void deploy handles Cloudflare and Void platform deployments. For Node.js, Bun, or Deno, run vite build and deploy dist/ using your hosting provider's workflow.
Ignored config fields
Void warns and ignores these Cloudflare-specific void.json fields on other targets:
inference.bindings: Cloudflare binding configurationremote: remote binding proxyworker: Cloudflare-specific configrouting.revalidate: edge caching, only on Cloudflare
If you enable auth through void/auth, void/client, or auth.ts, Void fails the build for non-CF targets. Use Better Auth directly for Node/Bun/Deno deployments.
Example: API + Pages
A typical Node.js Void app with API routes and React pages:
my-app/
pages/
layout.tsx
index.tsx
index.server.ts
about.tsx
about.server.ts
routes/
api/
hello.ts
package.json
vite.config.ts
void.json// void.json
{ "target": "node" }// vite.config.ts
import { defineConfig } from 'vite';
import { voidPlugin } from 'void';
import { voidReact } from '@void/react/plugin';
export default defineConfig({
plugins: [voidPlugin(), voidReact()],
});// routes/api/hello.ts
import { defineHandler } from 'void';
export const GET = defineHandler((c) => {
return c.json({ message: 'Hello from Node.js!' });
});# Development
npx vite dev
# Production
npx vite build
node dist/ssr/index.jsProgrammatic Usage
Since app.js exports the Hono app, you can import it into a custom server:
import app from './dist/ssr/app.js';
// Use with any Node.js HTTP framework
const response = await app.fetch(new Request('http://localhost/api/hello'));
console.log(await response.json()); // { message: "Hello from Node.js!" }This is useful for testing, embedding in Express/Fastify, or running in serverless environments that accept a fetch handler.