Skip to content

@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.

Terminal window
pnpm add @glandjs/express express

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.

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.

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().

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.

// TLS
const server = app.connectTo(ExpressBroker, {
https: { key: readFileSync('key.pem'), cert: readFileSync('cert.pem') },
})
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 })
}
}

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' })

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 }))

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 middleware
server.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 middleware
server.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()
})

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.

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')
})
await server.close()

The adapter closes the underlying Node server. The onAppShutdown and onModuleDestroy hooks already handle SIGTERM and SIGINT — see Lifecycle events.