Skip to content

@glandjs/koa@1.1.0-beta

@glandjs/koa

The Koa 3 transport.

Terminal window
# pnpm pnpm add @glandjs/koa
# npm npm install @glandjs/koa
# yarn yarn add @glandjs/koa
# bun bun add @glandjs/koa
Package @glandjs/koa
Version 1.1.0-beta
Repository glandjs/http
License MIT
Node >= 20
Dependencies @glandjs/core ^1.0.3-beta · @glandjs/events ^1.1.0 · @koa/router ^13.1.0 · @medishn/toolkit ^1.0.4 · koa ^3.0.0 · tslib ^2.8.1
Peer dependencies @glandjs/common ^1.0.3-beta · koa-bodyparser ^4.4.1 · koa-static ^5.0.0 · reflect-metadata ^0.2.2

@glandjs/koa is the Koa adapter for Gland’s HTTP layer.

Koa has the closest middleware model to Gland’s — so close that the adapter is the thinnest here, and await next() genuinely waits for the rest of the chain. A downstream throw reaches an upstream catch.

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

koa-static (static assets).

import { GlandFactory } from '@glandjs/core';
import { KoaBroker, type KoaContext } from '@glandjs/koa';
import { Get } from '@glandjs/http';
import { Controller, Module } from '@glandjs/common';
@Controller('/products')
class ProductController {
@Get('/:id')
async find(ctx: KoaContext) {
return ctx.call('db:product:find', ctx.params.id);
}
}
@Module({ controllers: [ProductController] })
class AppModule {}
const { app, shutdown } = await GlandFactory.create(AppModule);
const http = app.connectTo(KoaBroker, { poweredBy: false });
http.bodyParser({ limit: 1_000_000 });
http.use(async (ctx, next) => {
const started = Date.now();
await next(); // really waits
console.log(`${ctx.method} ${ctx.path} ${Date.now() - started}ms`);
});
http.useRaw(require('koa-static')('./public'));
http.listen(3000);

Koa is the model this layer was written against, so the bridge is a one-liner:

app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
ctx.status(503).json({ error: 'upstream unavailable' });
}
});

A downstream throw propagates through next() and reaches the catch — the full onion, with no caveats.

Paths arrive at the router already prefixed, because HttpServerAdapter.route() applies it. The adapter therefore mounts app.use(router.routes()) with no router prefix.

A Router mounted at /api with routes registered at /api/products serves /api/api/products, and 404s everything the application exists to answer. That is the single most common way a prefix goes wrong.

koa-bodyparser is loaded on request rather than bundled. Parsing is a security decision — limits, encodings, content types — and Koa’s answer to it is a well-known package rather than a rewrite.

app.bodyParser({ limit: 1_000_000 }); // 100 kB by default

Multipart is not handled here: streaming a file upload through a buffering parser is how a 10 MB video becomes a 10 MB heap spike. Use @koa/multer through useRaw.

  • poweredBy: false is a no-op. Koa sets no X-Powered-By; that is Express. A reverse proxy that adds one is the proxy’s configuration.
  • Signed cookies work if a signed-cookie middleware is mounted, and ctx.signedCookies is {} without one.
  • The 404 is mounted after the router, and renders the same problem document as every other adapter rather than Koa’s own body.
Export Kind
KoaBroker / KoaBrokerClass What app.connectTo() takes
KoaCore The application, with a typed koa getter
KoaAdapter The adapter
KoaContext, KoaState, GlandKoaContext The context

The full surface is in the API reference.

MIT