Node.js Development

Modern Node.js APIs with Hono, Bun, and Edge Runtime

By Domain India Team · DomainIndia EngineeringPublished 9 min read
Knowledge base article
Contents (13 sections)

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.

Key takeaways

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

FrameworkYearRuntimePhilosophy
Express2010Node.js onlyMature, minimal, large ecosystem
Fastify2016Node.jsUsually faster than Express, schema-first
Koa2013Node.jsExpress successor by same team, async-native
Hono2022Node/Deno/Bun/EdgeMulti-runtime, Web-Fetch-API-based
ElysiaJS2023Bun 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

MetricNode.js 24 LTSBun 1.xDeno 2.x
Install speedBaselineMany times fasterFast
Startup timeFastFastestFast
Built-in APIsGrowing (test runner, built-in SQLite, TypeScript type stripping)SQLite, WebSocket server, password hashing, Postgres clientKV, test runner, formatter, linter
Package mgmtnpm/yarn/pnpmbun install (reads package.json)JSR / npm
Production-ready 2026?YesYesYes

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

bash
# Bun (needs unzip)
curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
bun --version

Or stick with Node.js 24 LTS:

bash
# 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 nodejs

Step 2 — Scaffold Hono API

bash
mkdir myapi && cd myapi
bun init  # or: npm init -y
bun add hono
# or: npm install hono

Put 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:

typescript
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):

typescript
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):

typescript
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):

typescript
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):

bash
bun add drizzle-orm postgres
bun add -D drizzle-kit
typescript
import { 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:

ini
[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.target

Keep .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:

bash
npm install -D wrangler

wrangler.toml:

toml
name = "hono-api"
main = "src/worker.ts"
compatibility_date = "2026-09-01"
compatibility_flags = ["nodejs_compat"]

src/worker.ts (Workers entry):

typescript
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:

bash
npx wrangler deploy

Same 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):

text
Client ─► Cloudflare Worker
             │
  ├── /api/search   → Worker handles (cached, fast)
  ├── /api/health   → Worker handles
  └── /api/orders   → Forwards to VPS (needs DB)
typescript
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" throughputThroughput with a DB query
Express + Node.jsBaselineLimited by the database
Hono + Node.jsNoticeably higherLimited by the database
Hono + BunHighestLimited by the database
Hono + Cloudflare WorkersScales across the edgeDepends 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():

typescript
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

Node-only or Bun-only APIs in shared code
They break on Workers. Use Web APIs (Request, Response, Headers, fetch, crypto) in app.ts and keep runtime-specific code in the entry files.
Assuming Bun runs every Node package
Most mainstream libraries work; check native modules and test before you switch.
An old compatibility_date
Workers keep old behaviour for old dates. Set a recent date and update it deliberately.
Over-abstracting early
Start on one runtime. Hono keeps a later move cheap, so don't build for platforms you may never use.
SQLite with many writers
SQLite allows one writer at a time; several processes writing at once get lock errors. Use WAL mode, or PostgreSQL for multi-process deployments.
Environment not loaded in systemd
systemd doesn't read your shell profile. Use 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.

Run your Hono API on a server you control

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

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Modern Node.js APIs with Hono, Bun, and Edge Runtime