SpaceNode
The pipeline is the heart of SpaceNode's middleware. Unlike Express's next() chain, pipes are pure functions.
// Request comes in
// ↓
// [cors] → returns nothing, sets headers
// ↓
// [logger] → returns { after: fn } for timing
// ↓
// [auth] → returns { user: {...} } → merged into request
// ↓
// [dto:loginDto] → validates body, replaces cleaned data
// ↓
// Handler runs (has access to user, cleaned body)
// ↓
// After-hooks run (reverse order, LIFO)
| Return Value | Effect |
|---|---|
undefined / null | Continue to next pipe (no-op) |
{ key: value } | Merge properties into request object |
{ after: fn } | Register a post-handler hook |
{ user, after: fn } | Both: merge AND register hook |
Throw HttpError | Abort pipeline, send error |
Call send() | Abort pipeline (e.g. CORS preflight) |
Pipes cannot overwrite built-in request properties. If a pipe tries to return a key that conflicts with a built-in (e.g. body, send, params), it is silently skipped with a warning.
Protected keys:
method, path, url, params, query, headers, cookies, body, files, ipdb, configsend, check, guard, error, emit, setHeader, redirect, html, cookiebody, return { cleanedBody }. The auth pipe returns { user } which is safe because user is not a protected key.Pipes can return { after: fn } to register code that runs after the handler. After-hooks execute in reverse order (LIFO — like try/finally):
// Logger pipe implementation (simplified)
function loggerPipe(request) {
const start = Date.now()
return {
after: (statusCode) => {
const ms = Date.now() - start
console.log(`${request.method} ${request.path} → ${statusCode} (${ms}ms)`)
}
}
}
// Pipe as inline function in module.js
const addRequestId = (request) => {
return { requestId: crypto.randomUUID() }
// Now handler can access request.requestId
}
export default {
name: 'api',
prefix: '/api',
pipe: [addRequestId], // functions allowed!
routes: [...]
}
// Router middleware (router.use())
// ↓
// Global pipes (createApp config.pipe)
// ↓
// Module pipes (module.js pipe: [])
// ↓
// Route pipes (per-route array)
// ↓
// Handler
// ↓
// After-hooks (reverse order, LIFO)
The router supports global middleware via router.use(fn). These functions run on every matched route before all pipes:
app.router.use((request, services) => {
request.startTime = Date.now()
})
// Multiple middleware are stacked in order of registration
app.router.use(requestIdMiddleware)
app.router.use(metricsMiddleware)
createApp({ pipe: [...] })) are the recommended approach. Use router.use() when you need middleware that runs before pipe resolution.