What is Gland?
Gland is an event-first, protocol-agnostic framework for Node.js. It separates business intent from transport mechanics using four small primitives.
Gland is an event-first, protocol-agnostic framework for building server-side applications in TypeScript and JavaScript. Its central premise is to treat domain behaviour as composable events rather than as monolithic request handlers, so that business intent stays separate from transport mechanics.
The framework keeps its core deliberately small. There are four ideas to learn, and everything else in the ecosystem is built out of them.
The four primitives
Section titled “The four primitives”| Primitive | Responsibility |
|---|---|
| Module | A composition boundary that registers controllers, channels and other modules. |
| Controller | Receives an interaction, emits well-scoped domain events. Holds no side effects. |
| Channel | Subscribes to events and performs side effects: persistence, responses, outbound calls. |
| Broker | A protocol adapter that bridges an external transport to the event system. |
A complete application
Section titled “A complete application”Four files, one HTTP endpoint. This is the shape of every Gland application.
src/product.channel.ts — the side effect lives here.
import { Channel, On } from '@glandjs/common'
@Channel('product')export class ProductChannel { private products = new Map<string, { id: string; name: string }>()
@On('list') list() { return Array.from(this.products.values()) }}src/product.controller.ts — the controller only translates and emits.
import { Controller } from '@glandjs/common'import { Get } from '@glandjs/http'import type { ExpressContext } from '@glandjs/express'
@Controller('products')export class ProductController { @Get() list(ctx: ExpressContext) { return ctx.send(await ctx.call('product:list', {})) }}src/app.module.ts — wiring.
import { Module } from '@glandjs/common'import { ProductController } from './product.controller'import { ProductChannel } from './product.channel'
@Module({ controllers: [ProductController], channels: [ProductChannel],})export class AppModule {}src/main.ts — bootstrap and attach a transport.
import { GlandFactory } from '@glandjs/core'import { ExpressBroker } from '@glandjs/express'import { AppModule } from './app.module'
async function bootstrap() { const app = await GlandFactory.create(AppModule) const server = app.connectTo(ExpressBroker) server.json() server.listen(3000)}
void bootstrap()GlandFactory.create() scans the module graph, instantiates everything, runs the
lifecycle hooks and binds discovered routes and channels. connectTo() attaches a
protocol broker and hands you back the transport application object.
How a request flows
Section titled “How a request flows”- A broker receives a transport signal and builds a context object for it.
- The router matches the context to a controller method.
- The controller calls
ctx.emit(...)for fire-and-forget work, orctx.call(...)when it needs a result back, and then replies withctx.send(...). - The event is resolved to a channel handler through the channel registry and invoked with the payload.
- The handler’s return value travels back to the controller for
ctx.call().
The controller never learns which class handled the event, and the channel never learns which transport triggered it.
Why events instead of method calls
Section titled “Why events instead of method calls”Calling a service directly from a controller is the shortest possible path, and it is the one that hurts the most later. It couples HTTP to your domain, it hides side effects, and it makes the interesting question — what actually happened? — impossible to answer in a test.
Routing interaction through events inverts the dependency. The controller states an intent; a channel decides how to satisfy it. From that you get, for free:
- Replaceable side effects. Swap the persistence channel for an HTTP one and the controller is untouched.
- Cheap tests. Assert that a controller emitted
product:list. Call the channel with a fake context. No server, no network. - Fan-out. Several channels can answer the same event without the controller knowing they exist.
- One domain, many transports. The same modules run under any broker.
Where to go next
Section titled “Where to go next”- Why Gland? — the trade-offs, stated plainly.
- Architecture — how a bootstrap actually flows.
- Installation — get it running in a minute.
- First steps — build the example above, for real.