@glandjs/http@1.1.0-beta
Controllers and routes
Declaring routes, every method, the prefix rules.
# 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 controller is a class with a path and some decorated methods. It never imports
a service and never touches res.
Declaring routes
Section titled “Declaring routes”@Controller('/products') // from @glandjs/commonexport class ProductController { @Get() // GET /products list(ctx) { … }
@Get('/:id') // GET /products/:id find(ctx) { … }
@Post('/') // POST /products create(ctx) { … }}The path is the controller’s prefix plus the handler’s own path, and the core
composes them. @Controller('/') with @Get('/health') is GET /health.
Route decorators come from @glandjs/http, not from the adapter and not from
@glandjs/common. That is deliberate: a controller should not have to change
when the transport does, and a route declaration that lived in the adapter
package would.
Every method
Section titled “Every method”| Decorator | Method | Native on |
|---|---|---|
@Get @Post @Put @Patch @Delete @Head @Options |
the usual seven | all five |
@All |
every method | all five, expanded per adapter |
@Trace @Connect @Purge |
HTTP/1.1 and RFC 9111 | express, koa, hono, node |
@Search |
RFC 5323 | express, koa, hono, node; fastify too |
@Propfind @Proppatch @Mkcol @Copy @Move @Lock @Unlock |
WebDAV | express, koa, hono, node; fastify for most |
@Acl @Report |
WebDAV | express, koa, hono, node |
A path may be an array, and each entry becomes its own route:
@Get(['/summary', '/overview'])index(ctx) { … }How a method that a framework does not know is registered
Section titled “How a method that a framework does not know is registered”@Propfind() is a real, supported route — not a fallback that 404s. Each
adapter spells it differently:
- Express has seven verbs, so the route is registered with
all()plus a method guard. APROPFINDreaches the handler and aPUTfalls through. - Fastify calls
addHttpMethod()first, which is Fastify’s own extension point.PROPFINDandSEARCHare innode:http#METHODSand work;MKWORKSPACEis not, and the adapter warns at boot that the route is not available rather than failing the request later. - Koa and Hono take any method string. Nothing to do.
node:httpcompiles the pattern itself, so a method is just a string.
The global prefix
Section titled “The global prefix”app.connectTo(ExpressBroker, { prefix: '/api' }); // orapp.setGlobalPrefix('/api');Every route registered afterwards is prefixed. The application is responsible for applying it — the core does not know about it — which means:
- do not mount a router at the prefix as well. Paths are already prefixed, so
a
Routermounted at/apiserves/api/api/products - the prefix is applied once, even if set twice
ctx.pathis the path as the client sent it, with the prefix included, because the router is what matched
Route handlers
Section titled “Route handlers”A handler is (ctx) => unknown. Its return value is the response — see
Replies — and ctx.params holds what the router captured.
@Get('/:id/reviews/:reviewId')async review(ctx) { return ctx.call('db:review:find', { id: ctx.params.id, reviewId: ctx.params.reviewId });}ctx.params is populated before the handler runs, and re-synced at that moment
rather than when the context was built. Express fills req.params inside the
route handler and Koa’s router sets them on Koa’s own context, so a context that
a middleware created first has none until the router has matched — which is why
the pipeline re-reads them.
Routes without a controller
Section titled “Routes without a controller”app.get('/health', (ctx) => ({ ok: true }));app.post('/webhooks/:provider', handleWebhook);app.get is HttpCore.get and takes the same (path, action) pair as
@Get(). Use it for endpoints that are not part of a controller’s tree — health
checks, a webhook, a debug route.
The verb methods are declared explicitly rather than generated, because they are the API surface a reader is looking for:
get post put patch delete head optionsall trace connect purge searchpropfind proppatch mkcol copy move lock unlock acl reportAnything outside that list goes through registerRoute(method, path, action).
The route table
Section titled “The route table”for (const route of app.routes) { console.log(route.method, route.path, route.origin);}origin is where the route came from — decorator, manual, or replay — which
is how you tell a controller route from one you registered by hand. replay is
the interesting one: it means the core published it during bootstrap and the
adapter picked it up afterwards, which is the normal order.
app.settings.routeCount is the same number without the iteration.
When no route arrives
Section titled “When no route arrives”If a controller route is missing, the broker says so:
[HTTP:Broker] ERROR This HTTP adapter received no routes from the application binder. …@glandjs/core replays its route log to an adapter that attaches later — that replay is what makes `app.connectTo(Broker)` work at all, and it is not present in @glandjs/core <= 1.0.3-beta.That message means the core is older than the HTTP layer expects, or the
application module declares no controllers. It is deliberately loud: the
alternative is a healthy-looking server that 404s every controller route, with
nothing in the log to explain it.