Skip to Content

Last Updated: 3/9/2026


Node.js

Node.js  is the most popular JavaScript runtime for server-side applications. While Hono was originally designed for edge runtimes, it works excellently on Node.js with the @hono/node-server  adapter.

Requirements

Node.js version requirements:

  • 18.x → 18.14.1+
  • 19.x → 19.7.0+
  • 20.x → 20.0.0+
  • 21.x+ → Latest recommended

Quick Start

Create a new Hono app for Node.js:

npm create hono@latest my-app

Select nodejs when prompted.

Setup

1. Create Project

::: code-group

npm create hono@latest my-app cd my-app npm install
yarn create hono my-app cd my-app yarn
pnpm create hono@latest my-app cd my-app pnpm install
bun create hono@latest my-app cd my-app bun install

:::

2. Write Your Application

Edit src/index.ts:

import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => { return c.text('Hello Node.js!') }) app.get('/api/hello', (c) => { return c.json({ message: 'Hello from Node.js', timestamp: new Date().toISOString() }) }) serve(app)

3. Run Locally

Start the development server:

::: code-group

npm run dev
yarn dev
pnpm dev

:::

Access http://localhost:3000 in your browser.

Configuration

Change Port Number

Specify the port with the port option:

import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello!')) serve({ fetch: app.fetch, port: 8080, }) console.log('Server running on http://localhost:8080')

Graceful Shutdown

Handle shutdown signals properly:

import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello!')) const server = serve({ fetch: app.fetch, port: 3000, }) // Graceful shutdown on SIGINT (Ctrl+C) process.on('SIGINT', () => { console.log('Shutting down gracefully...') server.close() process.exit(0) }) // Graceful shutdown on SIGTERM process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down...') server.close((err) => { if (err) { console.error('Error during shutdown:', err) process.exit(1) } process.exit(0) }) })

Serving Static Files

Use serveStatic from @hono/node-server/serve-static:

Directory Structure

. ├── favicon.ico ├── src │ └── index.ts └── static ├── hello.txt ├── image.png └── styles └── main.css

Serve Static Directory

import { serve } from '@hono/node-server' import { serveStatic } from '@hono/node-server/serve-static' import { Hono } from 'hono' const app = new Hono() // Serve files from ./static at /static/* app.use('/static/*', serveStatic({ root: './' })) // Serve favicon app.use('/favicon.ico', serveStatic({ path: './favicon.ico' })) // Serve all files from ./static at root app.use('*', serveStatic({ root: './static' })) app.get('/api/hello', (c) => c.json({ message: 'API works!' })) serve(app)

Access files:

  • http://localhost:3000/static/hello.txt
  • http://localhost:3000/static/image.png
  • http://localhost:3000/favicon.ico

Rewrite Request Paths

Map URLs to different directories:

app.use( '/static/*', serveStatic({ root: './', rewriteRequestPath: (path) => path.replace(/^\/static/, '/public'), }) )

Now /static/image.png serves from ./public/image.png.

Accessing Node.js APIs

Access the raw Node.js IncomingMessage and ServerResponse objects:

import { Hono } from 'hono' import { serve, type HttpBindings } from '@hono/node-server' type Bindings = HttpBindings & { // Add custom bindings if needed } const app = new Hono<{ Bindings: Bindings }>() app.get('/client-info', (c) => { const remoteAddress = c.env.incoming.socket.remoteAddress const remotePort = c.env.incoming.socket.remotePort return c.json({ ip: remoteAddress, port: remotePort, headers: c.req.header(), }) }) serve(app)

HTTP/2 Support

Unencrypted HTTP/2

import { createServer } from 'node:http2' import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('HTTP/2!')) const server = serve({ fetch: app.fetch, createServer, port: 3000, })

Encrypted HTTP/2 (HTTPS)

import { createSecureServer } from 'node:http2' import { readFileSync } from 'node:fs' import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Secure HTTP/2!')) const server = serve({ fetch: app.fetch, createServer: createSecureServer, serverOptions: { key: readFileSync('./certs/localhost-key.pem'), cert: readFileSync('./certs/localhost-cert.pem'), }, port: 3443, }) console.log('HTTPS server running on https://localhost:3443')

Building for Production

Build the Application

::: code-group

npm run build
yarn build
pnpm run build
bun run build

:::

This compiles TypeScript to JavaScript in the dist/ directory.

Run Production Build

node dist/index.js

Docker Deployment

Dockerfile

Here’s a production-ready Dockerfile:

# Build stage FROM node:22-alpine AS builder RUN apk add --no-cache gcompat WORKDIR /app COPY package*.json tsconfig.json ./ COPY src ./src RUN npm ci && \ npm run build && \ npm prune --production # Runtime stage FROM node:22-alpine AS runner WORKDIR /app # Create non-root user RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 hono # Copy built application COPY --from=builder --chown=hono:nodejs /app/node_modules /app/node_modules COPY --from=builder --chown=hono:nodejs /app/dist /app/dist COPY --from=builder --chown=hono:nodejs /app/package.json /app/package.json USER hono EXPOSE 3000 CMD ["node", "/app/dist/index.js"]

Build and Run

# Build image docker build -t my-hono-app . # Run container docker run -p 3000:3000 my-hono-app

Docker Compose

docker-compose.yml:

version: '3.8' services: app: build: . ports: - "3000:3000" environment: - NODE_ENV=production - PORT=3000 restart: unless-stopped

Run with:

docker-compose up -d

Environment Variables

Access environment variables via process.env:

const app = new Hono() app.get('/config', (c) => { return c.json({ environment: process.env.NODE_ENV || 'development', apiUrl: process.env.API_URL || 'http://localhost:3000', }) })

Create .env file:

NODE_ENV=production API_URL=https://api.example.com DATABASE_URL=postgresql://localhost/mydb

Load with a package like dotenv:

import 'dotenv/config' import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() // Now process.env.DATABASE_URL is available

Process Management

PM2

Use PM2  for production process management:

npm install -g pm2

Create ecosystem.config.js:

module.exports = { apps: [{ name: 'hono-app', script: './dist/index.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000, }, }], }

Start with PM2:

pm2 start ecosystem.config.js pm2 save pm2 startup

Deployment Platforms

VPS / Cloud Servers

  • DigitalOcean - Droplets or App Platform
  • AWS EC2 - Virtual servers
  • Google Cloud Compute Engine
  • Azure Virtual Machines
  • Linode
  • Hetzner

Platform-as-a-Service

  • Railway - Zero-config deployment
  • Render - Easy Node.js hosting
  • Fly.io - Global application platform
  • Heroku - Classic PaaS

Best Practices

Use Environment Variables

Never hardcode secrets:

// ❌ Don't const dbUrl = 'postgresql://user:pass@localhost/db' // ✅ Do const dbUrl = process.env.DATABASE_URL

Handle Errors

app.onError((err, c) => { console.error('Error:', err) return c.json({ error: 'Internal Server Error' }, 500) })

Enable Compression

import { compress } from 'hono/compress' app.use('*', compress())

Set Security Headers

import { secureHeaders } from 'hono/secure-headers' app.use('*', secureHeaders())

Use a Reverse Proxy

In production, run Node.js behind nginx or Caddy:

nginx config:

server { listen 80; server_name example.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }

What’s Next