Skip to content
Open

v2 #316

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e049469
perf: optimize request body reading and URL construction
mgcrea Feb 10, 2026
20609a6
perf: optimize buildOutgoingHttpHeaders for common case
mgcrea Feb 10, 2026
eea7522
Merge pull request #301 from mgcrea/main
usualoma Mar 1, 2026
68a5815
fix: More strictly, determine when new URL() should be used (#310)
usualoma Mar 7, 2026
86504a1
refactor: improved compatibility with web standard Request object (#311)
usualoma Mar 12, 2026
647e3b1
Merge pull request #315 from honojs/pref/request-v2
yusukebe Mar 12, 2026
480cf2f
chore: format the code
yusukebe Mar 12, 2026
9878524
fix(request): return error object instead of throwing (#318)
usualoma Mar 12, 2026
474aabf
feat: end support Node.js v18 (#317)
yusukebe Mar 12, 2026
6180c3c
perf(request): cache method key (#319)
yusukebe Mar 12, 2026
f1744ce
perf(url): mark host with port `:` safehost (#320)
yusukebe Mar 12, 2026
4426ffc
feat(test): migrate Jest to Vitest (#303)
koralle Mar 12, 2026
0443bcb
build: migrate from tsup to tsdown (#302)
tommy-ish Mar 13, 2026
492deb8
fix: only build exported entrypoints (#323)
BlankParticle Mar 13, 2026
73c00c0
chore(package.json): add `--run` flag to `test` in prerelease (#324)
yusukebe Mar 17, 2026
1a62962
chore: bump dependencies (#325)
yusukebe Mar 17, 2026
e105d62
chore: fix `peerDependencies` in `bun.lock` (#326)
yusukebe Mar 17, 2026
04b98c4
feat!: obsolete vercel adapter (#335)
yusukebe Apr 5, 2026
b6ffb6d
Merge branch 'main' into v2
yusukebe Apr 5, 2026
0406dd1
test: follow the new vitest api
yusukebe Apr 5, 2026
4e6b7a5
chore: add `type:module` to `package.json` (#336)
yusukebe Apr 6, 2026
35c1dfc
feat: first class support for websockets (#328)
BlankParticle Apr 6, 2026
341359e
chore: correct `package.json` (#337)
yusukebe Apr 6, 2026
24a60a4
docs(readme): update benchmark with express (#338)
yusukebe Apr 6, 2026
7503265
v2: perf(request): optimize newHeadersFromIncoming and signal fast-pa…
GavinMeierSonos Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x, 22.x, 24.x]
node-version: [20.x, 22.x, 24.x]

steps:
- uses: actions/checkout@v4
Expand All @@ -27,9 +27,21 @@ jobs:
- run: bun install
- run: bun run format
- run: bun run lint
- run: bun run build
- run: bun run test

ci-build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: 24.x
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build

ci-windows:
runs-on: windows-latest

Expand Down
74 changes: 56 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
# Node.js Adapter for Hono

This adapter `@hono/node-server` allows you to run your Hono application on Node.js.
Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js.
It utilizes web standard APIs implemented in Node.js version 18 or higher.
Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js. It utilizes web standard APIs implemented in Node.js.

## Benchmarks

Hono is 3.5 times faster than Express.
Hono is 4.1 times faster than Express.

Express:

```txt
$ bombardier -d 10s --fasthttp http://localhost:3000/

Statistics Avg Stdev Max
Reqs/sec 16438.94 1603.39 19155.47
Latency 7.60ms 7.51ms 559.89ms
Reqs/sec 20803.37 1713.06 24910.85
Latency 6.01ms 5.21ms 451.37ms
HTTP codes:
1xx - 0, 2xx - 164494, 3xx - 0, 4xx - 0, 5xx - 0
1xx - 0, 2xx - 208131, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 4.55MB/s
Throughput: 5.75MB/s
```

Hono + `@hono/node-server`:
Expand All @@ -28,23 +27,17 @@ Hono + `@hono/node-server`:
$ bombardier -d 10s --fasthttp http://localhost:3000/

Statistics Avg Stdev Max
Reqs/sec 58296.56 5512.74 74403.56
Latency 2.14ms 1.46ms 190.92ms
Reqs/sec 85405.51 7250.65 102658.51
Latency 1.46ms 1.00ms 149.95ms
HTTP codes:
1xx - 0, 2xx - 583059, 3xx - 0, 4xx - 0, 5xx - 0
1xx - 0, 2xx - 854120, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 12.56MB/s
Throughput: 18.49MB/s
```

## Requirements

It works on Node.js versions greater than 18.x. The specific required Node.js versions are as follows:

- 18.x => 18.14.1+
- 19.x => 19.7.0+
- 20.x => 20.0.0+

Essentially, you can simply use the latest version of each major release.
It works on Node.js versions greater than 20.x.

## Installation

Expand Down Expand Up @@ -77,6 +70,34 @@ serve(app, (info) => {
})
```

## WebSocket

You can upgrade WebSocket connections with `upgradeWebSocket` from `@hono/node-server`.
To enable this, install `ws` (and `@types/ws`) in your project, then create and provide a `WebSocketServer` as shown in the example below.

```ts
import { serve, upgradeWebSocket } from '@hono/node-server'
import { WebSocketServer } from 'ws'
import { Hono } from 'hono'

const app = new Hono()

app.get(
'/ws',
upgradeWebSocket(() => ({
onMessage(event, ws) {
ws.send(event.data)
},
}))
)

const wss = new WebSocketServer({ noServer: true }) // important to create with `noServer: true`
serve({
fetch: app.fetch,
websocket: { server: wss },
})
```

For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`.

```sh
Expand Down Expand Up @@ -138,6 +159,23 @@ serve({
})
```

### `websocket`

provide a websocket server to enable websocket support.

```ts
import { serve, upgradeWebSocket } from '@hono/node-server'
import { WebSocketServer } from 'ws'

// ...
const wss = new WebSocketServer({ noServer: true })

serve({
fetch: app.fetch,
websocket: { server: wss },
})
```

## Middleware

Most built-in middleware also works with Node.js.
Expand Down
982 changes: 350 additions & 632 deletions bun.lock

Large diffs are not rendered by default.

9 changes: 0 additions & 9 deletions jest.config.js

This file was deleted.

93 changes: 53 additions & 40 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,64 +1,77 @@
{
"name": "@hono/node-server",
"version": "1.19.12",
"version": "2.0.0-rc.1",
"description": "Node.js Adapter for Hono",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/index.mjs",
"type": "module",
"types": "dist/index.d.mts",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./serve-static": {
"types": "./dist/serve-static.d.ts",
"require": "./dist/serve-static.js",
"import": "./dist/serve-static.mjs"
},
"./vercel": {
"types": "./dist/vercel.d.ts",
"require": "./dist/vercel.js",
"import": "./dist/vercel.mjs"
"import": {
"types": "./dist/serve-static.d.mts",
"default": "./dist/serve-static.mjs"
},
"require": {
"types": "./dist/serve-static.d.cts",
"default": "./dist/serve-static.cjs"
}
},
"./utils/*": {
"types": "./dist/utils/*.d.ts",
"require": "./dist/utils/*.js",
"import": "./dist/utils/*.mjs"
"import": {
"types": "./dist/utils/*.d.mts",
"default": "./dist/utils/*.mjs"
},
"require": {
"types": "./dist/utils/*.d.cts",
"default": "./dist/utils/*.cjs"
}
},
"./conninfo": {
"types": "./dist/conninfo.d.ts",
"require": "./dist/conninfo.js",
"import": "./dist/conninfo.mjs"
"import": {
"types": "./dist/conninfo.d.mts",
"default": "./dist/conninfo.mjs"
},
"require": {
"types": "./dist/conninfo.d.cts",
"default": "./dist/conninfo.cjs"
}
}
},
"typesVersions": {
"*": {
".": [
"./dist/index.d.ts"
"./dist/index.d.mts"
],
"serve-static": [
"./dist/serve-static.d.ts"
],
"vercel": [
"./dist/vercel.d.ts"
"./dist/serve-static.d.mts"
],
"utils/*": [
"./dist/utils/*.d.ts"
"./dist/utils/*.d.mts"
],
"conninfo": [
"./dist/conninfo.d.ts"
"./dist/conninfo.d.mts"
]
}
},
"scripts": {
"test": "node --expose-gc node_modules/jest/bin/jest.js",
"build": "tsup --external hono",
"watch": "tsup --watch",
"test": "vitest",
"build": "tsdown --external hono",
"watch": "tsdown --watch",
"postbuild": "publint",
"prerelease": "bun run build && bun run test",
"prerelease": "bun run build && bun run test --run",
"release": "np",
"lint": "eslint src test",
"lint:fix": "eslint src test --fix",
Expand All @@ -68,7 +81,7 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/honojs/node-server.git"
"url": "git+https://github.com/honojs/node-server.git"
},
"homepage": "https://github.com/honojs/node-server",
"author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
Expand All @@ -77,24 +90,24 @@
"access": "public"
},
"engines": {
"node": ">=18.14.1"
"node": ">=20"
},
"devDependencies": {
"@hono/eslint-config": "^1.0.1",
"@types/jest": "^29.5.3",
"@types/node": "^20.10.0",
"@types/supertest": "^2.0.12",
"@types/ws": "^8.18.1",
"@whatwg-node/fetch": "^0.9.14",
"eslint": "^9.10.0",
"hono": "^4.4.10",
"jest": "^29.6.1",
"hono": "^4.12.8",
"np": "^7.7.0",
"prettier": "^3.2.4",
"publint": "^0.1.16",
"publint": "^0.3.18",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"tsup": "^7.2.0",
"typescript": "^5.3.2"
"tsdown": "^0.20.3",
"typescript": "^5.3.2",
"vitest": "^4.0.18",
"ws": "^8.19.0"
},
"peerDependencies": {
"hono": "^4"
Expand Down
11 changes: 11 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class RequestError extends Error {
constructor(
message: string,
options?: {
cause?: unknown
}
) {
super(message, options)
this.name = 'RequestError'
}
}
6 changes: 0 additions & 6 deletions src/globals.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { serve, createAdaptorServer } from './server'
export { upgradeWebSocket } from './websocket'
export { getRequestListener } from './listener'
export { RequestError } from './request'
export type { HttpBindings, Http2Bindings, ServerType } from './types'
18 changes: 9 additions & 9 deletions src/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Http2ServerResponse } from 'node:http2'
import type { Writable } from 'node:stream'
import type { IncomingMessageWithWrapBodyStream } from './request'
import {
abortControllerKey,
abortRequest,
newRequest,
Request as LightweightRequest,
wrapBodyStream,
Expand All @@ -20,7 +20,6 @@ import {
buildOutgoingHttpHeaders,
} from './utils'
import { X_ALREADY_SENT } from './utils/response/constants'
import './globals'

const outgoingEnded = Symbol('outgoingEnded')
const incomingDraining = Symbol('incomingDraining')
Expand Down Expand Up @@ -343,13 +342,14 @@ export const getRequestListener = (

// Detect if request was aborted.
outgoing.on('close', () => {
const abortController = req[abortControllerKey] as AbortController | undefined
if (abortController) {
if (incoming.errored) {
req[abortControllerKey].abort(incoming.errored.toString())
} else if (!outgoing.writableFinished) {
req[abortControllerKey].abort('Client connection prematurely closed.')
}
let abortReason: string | undefined
if (incoming.errored) {
abortReason = incoming.errored.toString()
} else if (!outgoing.writableFinished) {
abortReason = 'Client connection prematurely closed.'
}
if (abortReason !== undefined) {
req[abortRequest](abortReason)
}

// incoming is not consumed to the end
Expand Down
Loading