@glandjs/http@1.1.0-beta
Differences between adapters
What each transport can and cannot do.
# pnpm pnpm add @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node# npm npm install @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node# yarn yarn add @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node# bun bun add @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node| Package | @glandjs/http |
| Version | 1.1.0-beta |
| Repository | glandjs/http |
| License | MIT |
| Node | >= 20 |
| Dependencies | @glandjs/core ^1.0.3-beta · @glandjs/events ^1.1.0 · @medishn/toolkit ^1.0.4 · tslib ^2.8.1 |
| Peer dependencies | @glandjs/common ^1.0.3-beta · reflect-metadata ^0.2.2 |
Everything in the contract behaves identically on all five transports, and
test/integration/adapters.spec.ts enforces it. This page is the honest
remainder: what each transport cannot do, and what to use instead.
The matrix
Section titled “The matrix”| express | fastify | koa | hono | node | |
|---|---|---|---|---|---|
| Framework | Express 5 | Fastify 5 | Koa 3 | Hono 4 | node:http |
| Runtime | Node | Node | Node | Node, edge | Node |
| Extra runtime deps | express |
fastify |
koa, @koa/router |
hono |
none |
await next() waits |
no | yes | yes | yes | yes |
Upstream catch sees a downstream throw |
no | yes | yes | yes | yes |
useRaw |
yes | no | yes | yes, headers only | no |
| A real catch-all route | yes | yes | yes | yes | yes |
| WebDAV verbs | via a guard | most | native | native | native |
PROPFIND, SEARCH |
yes | yes | yes | yes | yes |
MKWORKSPACE, UPDATE |
yes | no | yes | yes | yes |
| Signed cookies | with a middleware | no | with a middleware | no | no |
| Parses without declaring | no | removed by the adapter | no | no | no |
raw body (Buffer) |
yes | yes | yes | no | yes |
| Multipart | multer |
@fastify/multipart |
@koa/multer |
c.req.parseBody() |
your own |
| Static files | built in | @fastify/static |
koa-static |
hono/serve-static |
your own |
| Views | built in | @fastify/view |
@koa/views |
your own | your own |
close() drains |
yes | yes, native | yes | yes | yes |
Trusts X-Forwarded-* |
trustProxy |
trustProxy |
app.proxy |
runtime-dependent | trustProxy |
The four that differ
Section titled “The four that differ”Everything below is a consequence of the framework, not of Gland. Koa is the baseline: it has no row here because nothing about it needs one.
Express: await next() resolves early
Section titled “Express: await next() resolves early”Express’s next() is a hand-off. The bridge resolves as soon as Express moves
downstream, so an upstream try/catch cannot see a downstream throw.
Still works: next(err), the error handler, path scoping, ordering.
Does not: measuring or transforming anything after the hand-off, from upstream.
The Gland onion is real on Fastify (one composed preHandler hook), Koa, Hono
and node:http. See
Middleware → The Express caveat for
the workarounds.
Fastify: two methods have no equivalent
Section titled “Fastify: two methods have no equivalent”Fastify 5 routes only its own default set, and addHttpMethod() refuses
anything outside node:http#METHODS. PROPFIND and SEARCH are in that list
and work. MKWORKSPACE and UPDATE are not, and the adapter says so at boot:
[HTTP:Fastify] WARN Fastify cannot route "MKWORKSPACE" (Provided method is invalid!). The method is not in node:http#METHODS, so it has no Fastify equivalent. Use @glandjs/express, @glandjs/koa, @glandjs/hono or @glandjs/node if the application needs it.And useRaw() has no meaning: Fastify has no use(). The adapter warns and
points at app.fastify.register(...).
Fastify: the one parser that starts on
Section titled “Fastify: the one parser that starts on”Every adapter installs nothing until a parser is declared. Fastify is the
exception — JSON parsing is built in and enabled by default — so the adapter
calls removeContentTypeParser('application/json') when nothing was declared.
The consequence is that Fastify is the only transport whose boot order has to
care about it: the removal is applied in onInitialize through defer(),
because HttpCore calls bodyParser() from its constructor and Fastify refuses
to change its content type parsers once ready() has run.
See Bodies and uploads.
Hono: a fetch Response is immutable
Section titled “Hono: a fetch Response is immutable”Hono targets edge runtimes, so its Request and Response are the Fetch API’s.
That is a strength — the same code runs on Cloudflare Workers, Deno and Bun — and
it has two consequences.
c.res is replaced, not mutated. A useRaw middleware that sets a header
is merged into the adapter’s reply rather than overwriting it, so
c.header('x-request-id', …) still reaches the client. What it cannot do is
change the status or the body: a fetch Response is immutable and the adapter’s
is the one the handler’s return value describes.
No raw body. A fetch body is read as text, and re-buffering it would mean
holding the whole body twice. The adapter warns at boot and ctx.rawText carries
what was read.
listen() is Node-only. On an edge runtime there is nothing to bind, and the
application exports fetch instead:
export default http.hono; // Cloudflare Workers, Deno, Bunnode:http: no framework to reach into
Section titled “node:http: no framework to reach into”useRaw() reports that it was ignored — there is no use() to call. Static
files and body parsing are native to the adapter; multipart is your own
business.
In exchange there are no runtime dependencies at all, and the package is the reference implementation of the contract.
Per-adapter notes
Section titled “Per-adapter notes”Express
Section titled “Express”app.set('trust proxy', 1); // a setting Gland does not modelapp.useRaw(compression()); // fineapp.setViewEngine(require('ejs')); // built inctx.send('done') is text/plain, not Express’s default text/html — the
adapter sets it, so a string reply means the same thing on all five.
Fastify
Section titled “Fastify”await app.ready(); // plugins load asynchronouslyapp.fastify.register(require('@fastify/helmet'));app.json(); // nativeapp.listen(3000); await app.ready(); before anything that depends on a plugin
being present. logger is off by default, because Gland publishes the lifecycle
events and two loggers emitting the same line per request is worse than one.
app.koa.keys = ['a secret']; // for signed cookiesapp.useRaw(require('koa-static')('./public'));poweredBy: false is a no-op: Koa sets no X-Powered-By, and a reverse proxy
that adds one is the proxy’s configuration.
export default app.hono; // the edge exportapp.useRaw(async (c, next) => { c.header('x-a', '1'); await next();});useRaw is for observing and adding headers, and for short-circuiting by
returning your own Response.
node:http
Section titled “node:http”app.listen(3000, { https: { key, cert } }); // TLS, via ServerFactoryapp.useStaticAssets('./public'); // served from the built-in routerServerFactory.create() picks node:https when https is present — and passes
the request listener through, which is the bug it exists to have fixed: creating a
TLS server without a listener produces a server that never answers a request.
What is identical
Section titled “What is identical”The list is worth reading precisely, because it is what the layer promises.
Reply coercion: object → JSON, string → text (sniffed for HTML), number →
JSON, Buffer → bytes, readable → piped, SseStream → text/event-stream,
HttpReply → exactly as stated, undefined → nothing.
Errors: RFC 7807 problem details, with the message dropped for a non-HttpException.
CORS: one implementation, one set of headers, Vary: Origin by default, and a
credentials + origin: '*' contradiction rejected at boot.
Routing: a prefix applied exactly once; params populated before the handler
runs; a repeated query key becoming an array; a path that is case-sensitive.
Lifecycle: the same events, on the same lifecycle bus, with the duration computed after the response.
Shutdown: close() drains in-flight requests, is safe when the server was never
started, and reports the resolved port through http:server:listening.