Skip to content
Node.js · TypeScript · Event-driven

Build modular, testable backends where controllers express intent, channels own side effects, and brokers carry the transport.

npm install @glandjs/core @glandjs/common @glandjs/events
  • 3method emitter
  • 0runtime deps in the emitter
  • MITlicensed
product.controller.ts
import { 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.

Why Gland

Built for a domain, not a framework

Keep domain behavior independent from transport, make side effects explicit, and let small primitives compose into the application you actually need.

Event-driven by default

Controllers never call business logic. They emit domain events; channels decide what happens next. Side effects become observable and replaceable.

Protocol-agnostic

Transport lives in brokers. Run the same controllers and channels over HTTP today and over WebSocket or RPC tomorrow — only the adapter wiring changes.

Tiny primitives

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.

TypeScript-first

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.

Testable by construction

Assert the events a controller emitted. Call a channel handler with a fake context. Because side effects live in channels, your suites stay small.

Gradually adoptable

Install only the packages you need and mount Gland channels next to your existing HTTP layer. There is no big-bang migration.

How it fits together

One request, four clear responsibilities

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.

  1. 01Controller

    Receives the context, expresses intent and replies. It performs no side effects.

    ctx.call(‘db:product:all’)
  2. 02Typed event

    The contract between the two sides. A renamed event or payload is a compile error.

    EventTypes
  3. 03Channel

    Owns the side effect: persistence, audit logs, notifications. Easy to fake in tests.

    @Channel(‘db’)
  4. 04Broker

    Carries the transport. Swap HTTP for another protocol without touching the domain.

    ExpressBroker
Packages

Install only what you need

The ecosystem is split by responsibility, so the runtime stays small and your dependency graph stays understandable.

Browse packages →
@glandjs/core1.0.3-beta

Runtime

The dependency-injection container, module bootstrap, lifecycle hooks and the Context base class.

@glandjs/common1.0.3-beta

Decorators

The decorators and metadata keys that make up the DSL: Module, Controller, Channel, On and Inject.

@glandjs/events1.1.2

Eventing

The namespaced event broker: channels, mesh networking, request/response calls and async watchers.

@glandjs/emitter1.1.4

Primitive

A zero-dependency, tree-routed event emitter with wildcard matching. Three methods, nothing else.

@glandjs/http1.0.0-beta

Protocol

The HTTP layer: route decorators, HttpContext, the HttpCore application surface and the HttpBroker.

The path

From first import to a real application

Start with the mental model, build the smallest working app, then go deeper into controllers, channels, modules and brokers.

Build your first Gland app in minutes

Install the packages, declare one event map and ship a complete product API with channels.