JavaScript APIs in 2026 don't have to be tied to one runtime. This guide builds a small API with Hono, runs it on Bun or Node.js, and deploys the same code to a VPS and to Cloudflare Workers.
Hono is a lightweight framework built on the Web Fetch API, so one codebase runs on Node.js, Bun, Deno, Cloudflare Workers and other serverless platforms. Bun adds very fast installs and built-in SQLite, WebSocket and password-hashing APIs. Run the API under systemd on a VPS, push the same app to Cloudflare Workers for edge routes, and keep stateful work on the server with the database.
The 2026 Node.js landscape
| Framework | Year | Runtime | Philosophy |
|---|---|---|---|
| Express | 2010 | Node.js only | Mature, minimal, large ecosystem |
| Fastify | 2016 | Node.js | Usually faster than Express, schema-first |
| Koa | 2013 | Node.js | Express successor by same team, async-native |
| Hono | 2022 | Node/Deno/Bun/Edge | Multi-runtime, Web-Fetch-API-based |
| ElysiaJS | 2023 | Bun first (adapters for others) | Bun-native, end-to-end type safety |
Pick Hono for new greenfield APIs — the same app runs on Node.js (for example on a VPS), Bun, Deno, Cloudflare Workers, Vercel Functions and AWS Lambda, with only a small entry file per runtime.
Why Bun runtime
| Metric | Node.js 24 LTS | Bun 1.x | Deno 2.x |
|---|---|---|---|
| Install speed | Baseline | Many times faster | Fast |
| Startup time | Fast | Fastest | Fast |
| Built-in APIs | Growing (test runner, built-in SQLite, TypeScript type stripping) | SQLite, WebSocket server, password hashing, Postgres client | KV, test runner, formatter, linter |
| Package mgmt | npm/yarn/pnpm | bun install (reads package.json) | JSR / npm |
| Production-ready 2026? | Yes | Yes | Yes |
Bun wins on install speed and developer experience. Node.js is still the most proven runtime in production and the one every hosting tool supports.
Step 1 — Install Bun or Node.js on your VPS
# Bun (needs unzip)
curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
bun --versionOr stick with Node.js 24 LTS:
# AlmaLinux / Rocky
curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -
sudo dnf install -y nodejs
# Ubuntu / Debian
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash -
sudo apt install -y nodejsStep 2 — Scaffold Hono API
mkdir myapi && cd myapi
bun init # or: npm init -y
bun add hono
# or: npm install honoPut the app in src/app.ts, and keep a tiny entry file per runtime. Use process.env, which works on Node.js, Bun and (with nodejs_compat) Cloudflare Workers.
src/app.ts:
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { jwt } from 'hono/jwt';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
export const app = new Hono();
app.use('*', logger());
app.use('*', cors({ origin: ['https://yourcompany.com'] }));
// Public routes
app.get('/health', (c) => c.json({ status: 'ok', version: '1.0.0' }));
// Protected routes
app.use('/v1/*', jwt({ secret: process.env.JWT_SECRET!, alg: 'HS256' }));
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
app.post('/v1/users', zValidator('json', userSchema), async (c) => {
const { email, name } = c.req.valid('json');
const user = await createUser(email, name); // your DB call
return c.json(user, 201);
});
app.get('/v1/users/:id', async (c) => {
const user = await getUser(c.req.param('id'));
if (!user) return c.json({ error: 'Not found' }, 404);
return c.json(user);
});
// Error handler: log the details, don't send them to the client
app.onError((err, c) => {
console.error(err);
return c.json({ error: 'Internal server error' }, 500);
});src/bun.ts (Bun entry):
import { app } from './app';
export default {
port: Number(process.env.PORT) || 3000,
hostname: '127.0.0.1',
fetch: app.fetch,
};Run it with bun run --watch src/bun.ts.
src/node.ts (Node.js entry, via @hono/node-server):
import { serve } from '@hono/node-server';
import { app } from './app';
serve({ fetch: app.fetch, port: Number(process.env.PORT) || 3000, hostname: '127.0.0.1' });Install it with npm install @hono/node-server, run it in development with npx tsx src/node.ts, and compile with tsc (or a bundler) for production. Same app, different entry file.
Step 3 — Database with Bun's built-in SQLite or Drizzle
SQLite (Bun native, no dep):
import { Database } from 'bun:sqlite';
const db = new Database('data.db', { create: true });
db.run(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE,
name TEXT
)
`);
const insertUser = db.prepare('INSERT INTO users (id, email, name) VALUES (?, ?, ?)');
insertUser.run(crypto.randomUUID(), '[email protected]', 'Rajesh');
const users = db.prepare('SELECT * FROM users').all();PostgreSQL with Drizzle ORM (works everywhere):
bun add drizzle-orm postgres
bun add -D drizzle-kitimport { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').unique().notNull(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
// Usage:
await db.insert(users).values({ email: '[email protected]', name: 'Rajesh' });
const all = await db.select().from(users);Drizzle is type-safe, close to SQL, and works on Node.js, Bun and edge runtimes (with an HTTP or pooled driver). It needs no separate client-generation step.
Step 4 — Deploy to a VPS
systemd service /etc/systemd/system/hono-api.service:
[Unit]
Description=Hono API
After=network.target postgresql.service
[Service]
Type=simple
User=hono
WorkingDirectory=/opt/hono-api
ExecStart=/home/hono/.bun/bin/bun run src/bun.ts
Restart=on-failure
RestartSec=3
EnvironmentFile=/opt/hono-api/.env
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetKeep .env readable only by the hono user (chmod 600). Put nginx or Caddy in front for SSL; the Go deployment guide shows the same reverse-proxy pattern.
Step 5 — Deploy to Cloudflare Workers (same code!)
The magic of Hono: deploy the same app to Cloudflare Workers:
npm install -D wranglerwrangler.toml:
name = "hono-api"
main = "src/worker.ts"
compatibility_date = "2026-09-01"
compatibility_flags = ["nodejs_compat"]src/worker.ts (Workers entry):
import { app } from './app';
export default app;Store secrets with npx wrangler secret put JWT_SECRET. With nodejs_compat and a recent compatibility date, they appear in process.env; you can also read them from c.env. Then deploy:
npx wrangler deploySame business logic, now running on Cloudflare's edge network. bun:sqlite and other Bun-only APIs don't exist on Workers, so keep them out of shared code.
Pattern: Edge + origin
Route some requests to edge (fast, cheap), others to VPS (stateful):
Client ─► Cloudflare Worker
│
├── /api/search → Worker handles (cached, fast)
├── /api/health → Worker handles
└── /api/orders → Forwards to VPS (needs DB)app.get('/api/orders/*', async (c) => {
// Forward to VPS origin
return fetch(`https://origin.yourcompany.com${c.req.path}`, c.req.raw);
});Performance — what to expect
Public benchmarks consistently show the same shape; measure your own app before you decide:
| Stack | "Hello world" throughput | Throughput with a DB query |
|---|---|---|
| Express + Node.js | Baseline | Limited by the database |
| Hono + Node.js | Noticeably higher | Limited by the database |
| Hono + Bun | Highest | Limited by the database |
| Hono + Cloudflare Workers | Scales across the edge | Depends on the round trip to your database |
Hono on Bun gives you the most raw throughput. For database-bound workloads, the runtime matters much less: the database is the bottleneck.
Testing
Hono apps can be tested without starting a server, using app.request():
import { describe, test, expect } from 'bun:test';
import { app } from '../src/app';
describe('Health', () => {
test('returns ok', async () => {
const res = await app.request('/health');
expect(res.status).toBe(200);
const body = await res.json();
expect(body.status).toBe('ok');
});
});Run: bun test.
Common pitfalls
app.ts and keep runtime-specific code in the entry files.EnvironmentFile= explicitly.Running this on Domain India
- VPS: everything in this guide runs on a Domain India VPS, which is self-managed with full root access, from ₹553 a month excluding 18% GST. You install Bun or Node.js, the reverse proxy and the firewall yourself, and VPS plans include no backups or snapshots.
- App Platform: Node.js apps are detected automatically, so the Node.js entry of this API is a good fit; for Bun, deploy with your own Dockerfile. PostgreSQL and free SSL are included on every plan. There is no WebSocket support and no Redis add-on. See the App Platform guide.
- Shared hosting (cPanel, DirectAdmin): Setup Node.js App runs Node.js apps through Phusion Passenger; Bun is not available. See Deploy a Node.js app on shared hosting, and build your TypeScript locally before you upload.
FAQ
Hono, Express, or Fastify for new projects in 2026?
Hono — multi-runtime, future-proof, minimal. Express still fine for pure Node.js simplicity. Fastify if you need schema-first JSON validation and don't care about edge deployment.
Bun in production — is it ready?
For most greenfield APIs, yes: Bun is stable and widely used in production. Node.js remains more battle-tested and better supported by hosting tools, so choose Node.js for existing apps or where your platform only offers Node.
Drizzle vs Prisma?
Drizzle is TypeScript-first with a SQL-like API and no generate step, and it runs well on edge runtimes. Prisma has a more polished schema workflow, migrations tooling and a bigger ecosystem. Both are solid choices.
Can I run Bun on Domain India shared hosting?
No. Setup Node.js App on cPanel and DirectAdmin runs Node.js only. You can deploy the Node.js entry of a Hono app there, or run Bun on a VPS or on the App Platform with your own Dockerfile.
Hono on Cloudflare Workers — any limits?
Workers Free allows 100,000 requests a day with 10 ms of CPU time per request and 128 MB of memory. Workers Paid (from $5 a month) includes 10 million requests a month and much higher CPU limits; check Cloudflare's pricing page for current numbers. A VPS costs the same each month however many requests it serves, up to its capacity.
Ready to deploy? Compare VPS plans, read the App Platform guide, or open a support ticket with questions about a plan.
A self-managed Domain India VPS gives you full root access to run Bun or Node.js under systemd, behind nginx or Caddy.
See VPS plans