@glandjs/express@1.0.0-beta
@glandjs/express
The Express 5 adapter — ExpressBroker, ExpressContext, middleware with a Gland context, and the full application surface.
@glandjs/express is the reference adapter for Gland. It binds your controllers and
channels to a real Express 5 application and gives you back
the live app object.
pnpm add @glandjs/express expressWhy this adapter exists
Section titled “Why this adapter exists”Express is not your application — it’s just one way to deliver HTTP. Gland abstracts that detail.
This package is a bridge between the Express HTTP server and Gland’s internal architecture. It listens for requests via Express and transforms them into internal Gland events. Likewise, responses generated inside the Gland runtime are mapped back into Express’s response format.
Express’s API and ecosystem are fully supported — but once a request enters the Gland system it is treated like any other event in the broker-based system. Your business logic is not tied to Express, and can be reused with a different protocol or server without rewriting the core.
With @glandjs/express, HTTP is just another communication channel. All events are
routed through Gland’s centralised broker, which is what keeps the whole system
protocol-agnostic.
Attaching the adapter
Section titled “Attaching the adapter”import { GlandFactory } from '@glandjs/core'import { ExpressBroker } from '@glandjs/express'import type { EventTypes } from './shared/events.interface'
const app = await GlandFactory.create(AppModule)
const server = app.connectTo(ExpressBroker<EventTypes>)server.json()server.urlencoded({ extended: true })server.listen(3000, { host: '0.0.0.0', message: 'API ready on :3000' })Passing the event map as a type argument is what makes ctx.call(...) resolve to a
real type inside your controllers.
Exports
Section titled “Exports”export * from './broker'export * from './context'| Symbol | Description |
|---|---|
ExpressBroker<TEvents> |
The broker you pass to connectTo(). |
ExpressContext<TEvents> |
The context type your handlers receive. |
ExpressCore and ExpressAdapter are internal; you get the core instance’s type
inferred from connectTo().
ExpressBroker
Section titled “ExpressBroker”class ExpressBroker<TEvents extends EventRecord> extends HttpBroker< TEvents, ExpressCore<TEvents>, HttpApplicationOptions> { constructor(options?: HttpApplicationOptions) initialize(): ExpressCore<TEvents>}Inherits broker and instance from BrokerAdapter, and initialize() from
HttpBroker, which subscribes to gland:define:route and registers each discovered
route on the Express app.
// TLSconst server = app.connectTo(ExpressBroker, { https: { key: readFileSync('key.pem'), cert: readFileSync('cert.pem') },})ExpressContext
Section titled “ExpressContext”class ExpressContext<TEvents extends EventRecord> extends HttpContext< Request, Response, TEvents> { constructor(events: HttpEventCore<TEvents>, req: Request, res: Response)}A complete implementation of every HttpContext member, delegating to the underlying
Express req / res. params and host are populated at construction.
import type { ExpressContext } from '@glandjs/express'
@Controller('products')export class ProductController { @Get(':id') async getOne(ctx: ExpressContext<EventTypes>) { const { id } = ctx.params const product = await ctx.call('db:product:find', { id }) return ctx.send({ product }) }}The application surface
Section titled “The application surface”connectTo() returns an ExpressCore<TEvents> — the full
HttpCore surface, backed by a real Express app.
server.json()server.urlencoded({ extended: true })server.useStaticAssets('public')server.setGlobalPrefix('/api')server.enableCors({ origin: 'https://example.com', credentials: true })server.setViewEngine('ejs')server.setBaseViewsDir('views')
server.listen(3000, { host: '0.0.0.0', message: 'ready' })Routes outside a controller
Section titled “Routes outside a controller”Every verb HttpCore exposes is available directly, which is handy for health checks
or a legacy route mounted next to Gland’s.
server.get('/ping', (ctx) => ctx.send('pong'))server.post('/webhooks/stripe', (ctx) => ctx.send({ received: true }))Middleware
Section titled “Middleware”Gland-style middleware takes the context instead of (req, res, next). The adapter
detects this by arity and rewrites the function for you, so a two- or three-argument
middleware is treated as a Gland handler.
// 2 args -> Gland middlewareserver.use(async (ctx, next) => { const started = Date.now() ctx.state.requestId = crypto.randomUUID()
await next()
console.log(`${ctx.method} ${ctx.originalUrl} ${Date.now() - started}ms`)})
// 3 args -> also treated as Gland middlewareserver.use(async (ctx, next) => { const auth = ctx.getHeader('authorization') if (!auth) return ctx.send({ error: 'Unauthorized' }, 401) ctx.state.user = await verify(auth) return next()})Reply resolution
Section titled “Reply resolution”A handler can return the context or a plain value.
// Explicit@Get()list(ctx) { return ctx.send({ products: [] })}
// Implicit — the adapter replies with whatever came back@Get()list(ctx) { return { products: [] }}If nothing was sent by the time the handler returns, the adapter writes the value as
the response body. Streams — anything with a getStream() method — are piped instead
of serialised.
Error handling
Section titled “Error handling”import { HttpStatus } from '@medishn/toolkit'
@Get(':id')async getOne(ctx) { const product = await ctx.call('db:product:find', { id: ctx.params.id }) if (!product) return ctx.throw(HttpStatus.NOT_FOUND, { message: 'Not found' }) return ctx.send({ product })}Transport-level failures are emitted as a server:crashed event rather than thrown,
so you can log them in one place:
server.broker.on('server:crashed', ({ message, error }) => { logger.error({ message, error }, 'http')})Shutting down
Section titled “Shutting down”await server.close()The adapter closes the underlying Node server. The onAppShutdown and
onModuleDestroy hooks already handle SIGTERM and SIGINT — see
Lifecycle events.