Namespace:
[Loom]::[Adapter]::[HTTP]> Module:ApiModule> Dependencies:express
The HTTP Adapter implements a Controller-Service Separation pattern. It transforms the web server from a collection of callback functions into a structured, class-based handling system.
Routing in Link Loom is static and centralized. You do not define routes inside controllers. Instead, the router.js file acts as the "Switchboard" for the entire application.
/* src/routes/router.js */
const systemRoutes = require('./api/system/system.routes');
const router = {
...systemRoutes, // Spread domain-specific route files
// Direct Definition
hello: [
{
method: 'GET',
httpRoute: '/ping', // URL: /hello/ping
route: 'routes/api/hello.route', // Physical File
handler: 'ping', // Class Method
protected: false, // Middleware Flag
},
],
};Architectural Benefit:
- Visibility: You can see every endpoint in the system in one file.
- Portability: The router definition is JSON-serializable, allowing for easy export or static analysis.
Each "Route" file is actually a Controller Class. It is instantiated per request (or as a singleton depending on DI scope, though logically treated as stateless).
/* src/routes/api/hello.route.js */
class HelloRoute {
constructor(dependencies) {
this._services = dependencies.services;
this.EntityService = this._services.HelloService;
}
/**
* @param {Object} ctx - The Unified Request Context
* @param {Object} ctx.params - Merged body, query, and route params
* @param {Object} ctx.req - Original Express Request
* @param {Object} ctx.res - Original Express Response
*/
async ping(ctx) {
// 1. Unwrap Context
// 2. Delegate to Business Logic (Service)
const entityService = new this.EntityService(this._dependencies);
return entityService.ping(ctx);
}
}Why this pattern?
- Decoupling: The Controller (
HelloRoute) knows about HTTP (ctx), but the Service (HelloService) does not. - Testability: You can unit test the
pingmethod by mockingctx.
The ApiModule abstracts the raw Express req object into a cleaner ctx object to prevent massive function signatures.
| Property | Source | Description |
|---|---|---|
ctx.params |
req.body, req.query, req.params |
All input data merged into one object. Validated if protected: true. |
ctx.headers |
req.headers |
Raw headers. |
ctx.req |
req |
Fallback for low-level access (streaming, etc). |
ctx.res |
res |
Fallback for custom responses (redirects, files). |
For real-time server-to-client push, add streaming: true to a route config. The handler receives a stream object in its context instead of returning a JSON response.
{
method: 'GET',
httpRoute: '/live',
route: 'routes/api/events/events.route',
handler: 'stream',
protected: false,
streaming: true, // ← Activates SSE mode
}The pipeline is constructed dynamically for every endpoint:
- Body Limit (
body-parser): Injected ifbodyLimitis set. Overrides the global body-parser limit for this route only. - Storage (
multer): Injected ifsupportFile: true. - Validator: Injected if
protected: true. Validatesctx.paramsagainst the Service Schema. - Handler: The final execution wrapper that calls your Route Class.
graph LR
Req[Request] --> Router
Router -->|bodyLimit?| BodyParser[Body Parser Override]
BodyParser -->|supportFile?| Multer
Multer -->|protected?| Validator
Validator -->|valid| Handler
Handler --> RouteClass
RouteClass --> Service
Service --> DB
By default, all routes share the global body-parser limit (100KB unless configured otherwise via BODY_PARSER_LIMIT env var or server.bodyParserLimit config). Some endpoints — such as webhook receivers or data import APIs — may need to accept larger payloads.
Instead of increasing the global limit (which would expose all routes to large payloads), you can set a per-route limit using the bodyLimit property:
{
method: 'POST',
httpRoute: '/trigger/webhooks/:mode/:bindingId',
route: 'routes/api/webhooks/webhook.route',
handler: 'handleWebhook',
protected: false,
bodyLimit: '5mb', // Only this route accepts up to 5MB
}When bodyLimit is set on a route, the SDK injects a dedicated body-parser middleware before the route handler. This middleware parses the request body with the specified limit, overriding the global parser for that specific route.
The global parser still applies to all other routes — they remain protected by the default limit.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
method |
string |
Yes | — | HTTP method (GET, POST, PATCH, DELETE) |
httpRoute |
string |
Yes | — | Express route path (supports :params) |
route |
string |
Yes | — | Path to the Route class file |
handler |
string |
Yes | — | Method name to invoke on the Route class |
protected |
boolean |
Yes | — | Whether JWT validation middleware is applied |
supportFile |
boolean |
No | false |
Enables multer file upload middleware |
streaming |
boolean |
No | false |
Enables SSE mode (docs) |
bodyLimit |
string |
No | (global) | Max body size for this route ('1mb', '5mb', etc.) |
| Scenario | Recommendation |
|---|---|
| Webhook endpoints receiving external payloads (emails, events) | bodyLimit: '5mb' |
| File metadata or batch import APIs | bodyLimit: '2mb' |
| Standard CRUD endpoints | No bodyLimit needed (use global default) |
| Public-facing APIs with untrusted input | Keep the default 100kb |
// src/routes/api/workflow/triggers.routes.js
module.exports = {
'workflow-orchestration': [
{
method: 'POST',
httpRoute: '/trigger/webhooks/:mode/:flowDefinitionId',
route: 'routes/api/workflow/triggers/webhook.route',
handler: 'handleWebhook',
protected: false,
bodyLimit: '5mb', // Email payloads can exceed 100KB
},
{
method: 'GET',
httpRoute: '/trigger/webhooks/:bindingId/status',
route: 'routes/api/workflow/triggers/webhook.route',
handler: 'getStatus',
protected: true,
// No bodyLimit — uses global default (GET has no body anyway)
},
],
};