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-appSelect nodejs when prompted.
Setup
1. Create Project
::: code-group
npm create hono@latest my-app
cd my-app
npm installyarn create hono my-app
cd my-app
yarnpnpm create hono@latest my-app
cd my-app
pnpm installbun 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 devyarn devpnpm 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.cssServe 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.txthttp://localhost:3000/static/image.pnghttp://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 buildyarn buildpnpm run buildbun run build:::
This compiles TypeScript to JavaScript in the dist/ directory.
Run Production Build
node dist/index.jsDocker 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-appDocker Compose
docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
restart: unless-stoppedRun with:
docker-compose up -dEnvironment 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/mydbLoad 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 availableProcess Management
PM2
Use PM2 for production process management:
npm install -g pm2Create 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 startupDeployment 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_URLHandle 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
- Bun - Faster alternative to Node.js
- Docker Documentation - Container deployment
- Best Practices - Application architecture