Skip to content

@glandjs/http@1.1.0-beta

Getting started

A working service, from install to first request.

Terminal window
# pnpm pnpm add @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node
# npm npm install @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node
# yarn yarn add @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node
# bun bun add @glandjs/http @glandjs/express @glandjs/fastify @glandjs/koa @glandjs/hono @glandjs/node
Package @glandjs/http
Version 1.1.0-beta
Repository glandjs/http
License MIT
Node >= 20
Dependencies @glandjs/core ^1.0.3-beta · @glandjs/events ^1.1.0 · @medishn/toolkit ^1.0.4 · tslib ^2.8.1
Peer dependencies @glandjs/common ^1.0.3-beta · reflect-metadata ^0.2.2

A working Gland HTTP service, from pnpm to a request. Express, because it is the most familiar; swap one import for any of the other four and nothing else changes.

Terminal window
npm install @glandjs/core @glandjs/common @glandjs/http @glandjs/express reflect-metadata

reflect-metadata is a peer dependency, not an optional one. @glandjs/core discovers a provider’s constructor parameters from compiler-emitted metadata, and that metadata is only produced for decorated classes — so a plain service with no @Injectable() receives undefined for every dependency.

Two compiler options are also not optional:

tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
},
}

A controller is a class with a path and some decorated methods. It never imports a service and never touches res.

import { Controller, Channel, Module, On } from '@glandjs/common';
import { Get, Post, HttpReply } from '@glandjs/http';
import { HttpStatus } from '@medishn/toolkit';
import type { ExpressContext } from '@glandjs/express';
interface Events {
'math:add': { a: number; b: number };
'math:sum': number;
}
@Controller('/products')
export class ProductController {
@Get('/:id')
async find(ctx: ExpressContext<Events>) {
const product = await ctx.call('db:product:find', { id: ctx.params.id });
if (!product) return ctx.throw(HttpStatus.NOT_FOUND, { detail: 'No such product' });
return product; // an object becomes JSON
}
@Post('/')
async create(ctx: ExpressContext<Events>) {
const product = await ctx.call('db:product:create', ctx.body);
return HttpReply.json({ product }, { status: 201, headers: { location: `/products/${product.id}` } });
}
}
@Channel('math')
export class MathChannel {
@On('add')
add({ a, b }: { a: number; b: number }) {
return a + b;
}
}
@Module({ controllers: [ProductController], channels: [MathChannel] })
export class AppModule {}

Two things to notice.

ctx.call('db:product:find', …) reaches code by name. The controller does not import the channel that holds the data. That indirection is what lets one implementation be reused by a WebSocket, a queue consumer, or a CLI — and it is what makes a call observable without patching the callee.

The return value is the response. return product is JSON. ctx.throw ends the request with a problem document. There is no res, and no return res.json.

import { GlandFactory } from '@glandjs/core';
import { ExpressBroker } from '@glandjs/express';
async function main() {
const { app, shutdown } = await GlandFactory.create(AppModule);
const http = app.connectTo(ExpressBroker, { poweredBy: false });
http.json(); // parse JSON bodies
http.enableCors({ origin: 'https://example.com' });
http.use(async (ctx, next) => {
const started = Date.now();
await next();
console.log(`${ctx.method} ${ctx.path} ${Date.now() - started}ms`);
});
http.listen(3000, { host: '0.0.0.0' });
const stop = async (signal: string) => {
await http.close();
await shutdown(signal);
process.exit(0);
};
process.on('SIGTERM', () => void stop('SIGTERM'));
process.on('SIGINT', () => void stop('SIGINT'));
}
main().catch((error) => {
console.error('Failed to start:', error);
process.exit(1);
});
Terminal window
curl localhost:3000/products/1

Three rules cover almost everything.

Configure after connectTo, listen last. connectTo() produces the application, so it has to come first — and the routes it registers are buffered until listen(), which is what makes http.use() afterwards still land in front of them. Ordering.

use is the Gland onion; useRaw is the framework’s. They interleave in call order.

app.use(async (ctx, next) => { … }); // (ctx, next) => …
app.use('/api', rateLimit); // path-scoped
app.useRaw(compression()); // framework middleware

Return a value, or call ctx.…. Both answer the request; doing both is harmless, because the pipeline skips a context that has already answered.

import { ExpressBroker } from '@glandjs/express';
import { FastifyBroker } from '@glandjs/fastify';
const http = app.connectTo(ExpressBroker, { poweredBy: false });
const http = app.connectTo(FastifyBroker, { poweredBy: false });

That is the whole change, and the controller does not move. Fastify loads its plugins asynchronously, so add one line:

http.listen(3000);
await http.ready();
Guide Covers
Choosing an adapter The five, and which one to reach for
Controllers and routes Every method, the prefix rules, WebDAV
The context Every member, grouped by what you are doing
Middleware The onion, and the one adapter that fakes it
Replies What a handler may return, and what goes on the wire
Errors Problem details, and what is never leaked
Bodies and uploads Nothing is parsed until you declare it
Lifecycle events Metrics and logs that stay out of the request path
Deployment Proxies, graceful shutdown, health checks
Testing Unit vs. integration, and the contract suite

If you are upgrading rather than starting fresh, read Migrating to 1.1 first — ctx.getHeader() changed which side of the exchange it reads, and that is the one silent behaviour change.