Skip to content

@glandjs/express@1.1.0-beta

@glandjs/express

The Express 5 transport.

Terminal window
# pnpm pnpm add @glandjs/express
# npm npm install @glandjs/express
# yarn yarn add @glandjs/express
# bun bun add @glandjs/express
Package @glandjs/express
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 · express ^5.1.0 · tslib ^2.8.1
Peer dependencies @glandjs/common ^1.0.3-beta · reflect-metadata ^0.2.2

Express is not your application — it is just one way to deliver HTTP. Gland abstracts that detail.

@glandjs/express is the Express adapter for Gland’s HTTP layer. It contributes an Express application, an ExpressContext, and nothing else — the onion, the request lifecycle, the reply coercion and the error rendering all come from @glandjs/http, so the same controller runs unchanged on Fastify, Koa, Hono or node:http.

A controller never imports Express. A route is declared with a decorator from @glandjs/http, and the adapter works out how to write the reply to the socket.

Terminal window
npm install @glandjs/core @glandjs/common @glandjs/http @glandjs/express reflect-metadata
import { GlandFactory } from '@glandjs/core';
import { ExpressBroker, type ExpressContext } from '@glandjs/express';
import { Get } from '@glandjs/http';
import { Controller, Module } from '@glandjs/common';
@Controller('/products')
class ProductController {
@Get('/:id')
async find(ctx: ExpressContext) {
return ctx.call('db:product:find', ctx.params.id); // an object becomes JSON
}
}
@Module({ controllers: [ProductController] })
class AppModule {}
const { app, shutdown } = await GlandFactory.create(AppModule);
const http = app.connectTo(ExpressBroker, { poweredBy: false });
http.json();
http.use(async (ctx, next) => {
const started = Date.now();
await next();
console.log(`${ctx.method} ${ctx.path} ${Date.now() - started}ms`);
});
http.useRaw(compression()); // framework middleware, unmolested
http.listen(3000);

await next() resolves early. Express’s next() is a hand-off, not a call. The bridge resolves as soon as Express moves downstream, so an upstream try/catch cannot see a downstream throw. The error still reaches the error handler — next(err) is called for you — but you cannot measure or transform what happened after the hand-off.

app.use(async (ctx, next) => {
const started = Date.now();
await next(); // the hand-off, not the route
metrics.timing('route', Date.now() - started);
});

The transport-agnostic alternative, which works everywhere:

app.on(HttpEvent.RequestEnd, ({ method, path, status, duration }) => {
metrics.timing('http.request', duration, { method, path, status });
});

app.use is split in two. Gland’s use() is a promise-based onion; Express’s is a synchronous hand-off. Rather than infer intent from a function’s arity — which the previous version did, and which silently mis-typed every Express handler — the two are separate:

app.use((ctx, next) => …); // Gland, on the onion
app.useRaw(compression()); // Express, unmolested

ctx.send('done') is text/plain. Express’s res.send(string) defaults to text/html, which would make a text reply render as a page in a browser. The adapter sets the type, so a string means the same thing on all five adapters.

Extended methods use a guard. Express 5 exposes seven verbs, so @Propfind() is registered with all() plus a method check — a real route that works, rather than a TypeError: router.propfind is not a function at boot.

app.instance.set('trust proxy', 1); // a setting Gland does not model
app.set('etag', 'strong'); // the same thing, typed

That is the right answer for framework settings. It is the wrong answer for routing, body parsing or CORS, because those are the parts the abstraction owns.

Export Kind
ExpressBroker / ExpressBrokerClass What app.connectTo() takes
ExpressCore The application, with instance typed and set()
ExpressAdapter The adapter
ExpressContext The context
EXPRESS_VERBS, ExpressApp Extras

The full surface is in the API reference.

MIT