Runtime
The dependency-injection container, module bootstrap, lifecycle hooks and the Context base class.
Build modular, testable backends where controllers express intent, channels own side effects, and brokers carry the transport.
npm install @glandjs/core @glandjs/common @glandjs/eventsimport { Controller } from '@glandjs/common'import { Get } from '@glandjs/http'import type { ExpressContext } from '@glandjs/express'import type { EventTypes } from '../shared/events.interface'
@Controller('products')export class ProductController { @Get() async list(ctx: ExpressContext<EventTypes>) { const products = await ctx.call('db:product:all', {}) return ctx.send({ products }) }}Controllers reply. They never touch the database.
import { Channel, On } from '@glandjs/common'import type { Product } from '../shared/events.interface'
@Channel('db')export class Database { private products = new Map<string, Product>()
@On('product:all') async allProducts(): Promise<Product[]> { return Array.from(this.products.values()) }}Channels own side effects, addressed as db:product:all.
import 'reflect-metadata'import { GlandFactory } from '@glandjs/core'import { ExpressBroker } from '@glandjs/express'import { AppModule } from './app.module'import type { EventTypes } from './shared/events.interface'
async function bootstrap() { const app = await GlandFactory.create(AppModule) const server = app.connectTo(ExpressBroker<EventTypes>) server.listen(3000, { message: 'API ready on :3000' })}void bootstrap()Brokers carry the transport. Swap the adapter, keep the app.
Keep domain behavior independent from transport, make side effects explicit, and let small primitives compose into the application you actually need.
Controllers never call business logic. They emit domain events; channels decide what happens next. Side effects become observable and replaceable.
Transport lives in brokers. Run the same controllers and channels over HTTP today and over WebSocket or RPC tomorrow — only the adapter wiring changes.
The emitter is three methods and a couple of kilobytes. The broker adds channels, mesh networking and async watchers on top of it. Nothing is hidden.
Event names and payload shapes live in one typed map. A renamed event or a changed payload is a compile error, not a production incident.
Assert the events a controller emitted. Call a channel handler with a fake context. Because side effects live in channels, your suites stay small.
Install only the packages you need and mount Gland channels next to your existing HTTP layer. There is no big-bang migration.
Every request follows the same path. Each step has a single job, so you always know where a behavior lives and what to replace when it changes.
Receives the context, expresses intent and replies. It performs no side effects.
ctx.call(‘db:product:all’)The contract between the two sides. A renamed event or payload is a compile error.
EventTypesOwns the side effect: persistence, audit logs, notifications. Easy to fake in tests.
@Channel(‘db’)Carries the transport. Swap HTTP for another protocol without touching the domain.
ExpressBrokerThe ecosystem is split by responsibility, so the runtime stays small and your dependency graph stays understandable.
Runtime
The dependency-injection container, module bootstrap, lifecycle hooks and the Context base class.
Decorators
The decorators and metadata keys that make up the DSL: Module, Controller, Channel, On and Inject.
Eventing
The namespaced event broker: channels, mesh networking, request/response calls and async watchers.
Primitive
A zero-dependency, tree-routed event emitter with wildcard matching. Three methods, nothing else.
Protocol
The HTTP layer: route decorators, HttpContext, the HttpCore application surface and the HttpBroker.
Adapter
The Express 5 adapter. Wires Gland controllers and channels to a real HTTP server.
Start with the mental model, build the smallest working app, then go deeper into controllers, channels, modules and brokers.
Install the packages, declare one event map and ship a complete product API with channels.