@ailura/nestjs-hono-adapter
An HTTP adapter that runs a NestJS application on Hono, with no Express or Fastify underneath. The core is runtime-neutral; a transport decides how it is served: bunServer() (Bun's own server, the default), nodeServer() for @hono/node-server, or fetchServer() for a host that serves a Web Request/Response.
Problem: Nest has no official Hono adapter. The two published alternatives target Nest 11 and answer incorrectly — one returns a success status to a handler that threw, the other writes every response twice.
Solution: This package implements the Nest 11/12 AbstractHttpAdapter contract directly on Hono: routes are registered on a Hono application, and Hono's Web Request/Response are translated to and from the objects Nest reads and writes.
Install
bun add @ailura/nestjs-hono-adapter hono
@nestjs/common, @nestjs/core, hono, rxjs and @hono/node-server are peers, so the application decides their versions. @hono/node-server is optional: only the ./node-server transport reaches it, and a Bun or fetch deployment never loads it. Use npm install or pnpm add in place of bun add as needed.
The optional capability lives in a package of its own, with a single entrypoint, so importing it loads none of the others:
| Package | For | Installs |
|---|---|---|
@ailura/nestjs-platform-hono-multipart | uploads | busboy |
WebSockets are not a capability but a compatibility bridge: @ailura/nestjs-platform-hono-ws fits Nest's own @nestjs/platform-ws WsAdapter to Hono, so you install the package and write no import of it — app.useWebSocketAdapter(new WsAdapter(app)) is the whole wiring. Socket.IO is the same, through @ailura/nestjs-platform-hono-socket-io and the stock @nestjs/platform-socket.io IoAdapter. See docs/compatibility.md.
A socket.io gateway is served by the Bun transport's own server where it can bind an engine, and by the Node transport's http.Server otherwise; the fetch transport has neither and is refused by name.
Companion packages
| Package | What it does | When to install |
|---|---|---|
@ailura/nestjs-hono-adapter | the adapter itself | always |
@ailura/nestjs-platform-hono-multipart | multipart uploads via busboy | upload routes |
@ailura/nestjs-platform-hono-graphql | @nestjs/apollo ApolloDriver fitted to Hono | GraphQL drivers |
@ailura/nestjs-platform-hono-ws | @nestjs/platform-ws WsAdapter fitted to Hono | WebSocket gateways |
@ailura/nestjs-platform-hono-socket-io | @nestjs/platform-socket.io IoAdapter fitted to Hono | Socket.IO gateways |
@ailura/nestjs-platform-hono-serve-static | serve-static fitted to Hono | static assets |
And inside this repository:
- docs/architecture.md — layer map and boundaries
- docs/compatibility.md — the five bridges and their seam
- docs/deployment.md — Bun, Node and fetch hosts
- docs/platform-differences.md — parity with Express and Fastify
Migrating to 2.x
Five things break a deployment that tracked @ailura/[email protected]: ./ws moved, the default transport is no longer the Node one, HonoWsAdapter is gone, security headers are off by default, and the package is ESM only.
./ws and the HonoWsAdapter it exported are gone. WebSockets are served by the stock @nestjs/platform-ws WsAdapter, fitted to Hono by @ailura/nestjs-platform-hono-ws; install that package, which the compatibility region discovers, so a deployment writes no import of it and names the stock class instead — and installs @nestjs/platform-ws in place of @hono/node-ws.
| Old | New |
|---|---|
@ailura/nestjs-hono-adapter/ws | @ailura/nestjs-platform-hono-ws, installed alone |
new HonoWsAdapter(app) | new WsAdapter(app) from @nestjs/platform-ws |
Only a gateway's path is honoured; the wire protocol is platform-ws's own.
The zero-configuration bootstrap now serves through bunServer() where it served through @hono/node-server; a deployment on Node names the transport explicitly (Use).
ServerAdapterOptions.overrideGlobalObjects is gone and nodeServer() carries it, defaulting to true, the fastest path and the behaviour the implicit Node transport had; the replacement of the global Request and Response is process-wide and not restored, so a deployment that wants the globals left alone asks by name: nodeServer({ overrideGlobalObjects: false }).
secureHeaders defaulted to true and now defaults to false; a deployment that relied on the default sets secureHeaders: true.
The Bun transport resolves hono/bun in a top-level await so the root entrypoint still imports on Node, which makes the package ESM only: require() throws, import works on Node and Bun.
./multipart and ./socket-io were added on main after 1.4.1 and never published, so nobody has to migrate for them; a deployment that tracked main and imported one installs the matching package above.
Use
import { NestFactory } from '@nestjs/core';
import { ServerAdapter } from '@ailura/nestjs-hono-adapter';
import { bunServer } from '@ailura/nestjs-hono-adapter/bun-server';
import { AppModule } from './app.module.ts';
const app = await NestFactory.create(
AppModule,
new ServerAdapter({
bodyLimit: 2 * 1024 * 1024,
transport: bunServer(),
trustProxy: true,
}),
);
app.enableCors({
credentials: true,
origin: ['https://app.example.com'],
});
await app.listen(3000);
new ServerAdapter() with no transport serves through Bun's own server. A deployment on Node names it:
import { nodeServer } from '@ailura/nestjs-hono-adapter/node-server';
new ServerAdapter({ transport: nodeServer() });
nodeServer() takes autoCleanupIncoming (default true), which drains a request whose body was never read, and overrideGlobalObjects (default true), which asks @hono/node-server to replace the process's Request and Response with its lighter classes — the fastest path, and process-wide; pass false to leave the globals alone. Both only reach @hono/node-server, so they change nothing for a deployment that serves through bunServer().
On a fetch host — Cloudflare Workers, Vercel Functions or Deno — the transport takes no port and the deployment exports the handler:
import { fetchHandler, fetchServer } from '@ailura/nestjs-hono-adapter/fetch';
const adapter = new ServerAdapter({ transport: fetchServer() });
const app = await NestFactory.create(AppModule, adapter);
await app.init();
export default { fetch: fetchHandler(adapter.getHono()) };
listen() on the fetch server is refused, passing an error to the callback or throwing when none is given. The fetch handler has no client address by default and ignores cf-connecting-ip; pass fetchHandler(app, { clientAddress }) with a resolver for a trusted, host-supplied address, and read a header there only if the host guarantees clients cannot forge it. x-forwarded-for still supplies ip when trustProxy trusts the proxy. See fetch hosts.
getType() answers hono, the value ecosystem packages branch on.
For in-process tests, call await app.init() before app.inject(). String targets accept relative paths and absolute URLs; query appends to the existing query. GET and HEAD ignore payload/body; other methods send them as the request body.
setOnRequestHook() continues the request when the hook calls done or its returned promise/thenable resolves, whichever happens first. setOnResponseHook() runs when the chain settles, including when the request hook throws or rejects; it waits up to one second for response-hook work before releasing the response, and later rejections are still logged. See instrumentation hooks.
Options
app.set('views', directory) accepts a string or a string array, searched in order; app.set('view engine', extension) accepts a string. View data combines setViewEngine({ defaultContext }), setLocal() values, and handler data, in that order, preserving locals including keys such as __proto__.
| Option | Type | Default | Effect |
|---|---|---|---|
bodyLimit | number | 1 MiB | Largest non-multipart request body, in bytes; 0 removes the limit. Multipart parsing uses upload-route limits when the multipart package is enabled; declared lengths are still checked. |
rawBody | boolean | false | Keep the bytes that were read in NestRequest.rawBody |
secureHeaders | boolean | object | false | Install hono/secure-headers, with its defaults or with the given options; off by default |
transport | Transport | — | The runtime: bunServer(), nodeServer() or fetchServer() |
trustProxy | boolean | number | string[] | false | Read x-forwarded-proto, x-forwarded-for and x-forwarded-host |
views | object | — | The engine a @Render() handler renders with, and where templates are read from |
app.useBodyParser() accepts json, urlencoded, text and raw; unsupported names are rejected. Parsing follows the request's content type, and limit applies to every parser and route; omitting limit still enables parsing and applies the raw-body setting, leaving the existing size limit intact.
Documentation
Architecture, deployment, parity and the API reference live in the repository's docs/ directory.
Requirements
- Bun 1.2+ (toolchain, and the runtime the default transport serves on), or Node 22.12+.
@hono/node-server, whichnodeServer()serves through; it is an optional peer, and the default transport is Bun's own server.- Nest 11 or 12.
- Hono 4.
License
MIT