Pipeline System

The pipeline is the heart of SpaceNode's middleware. Unlike Express's next() chain, pipes are pure functions.

How It Works

//  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)

Pipe Return Values

Return ValueEffect
undefined / nullContinue 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 HttpErrorAbort pipeline, send error
Call send()Abort pipeline (e.g. CORS preflight)

Protected Keys

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:

Use custom key names — Instead of overwriting body, return { cleanedBody }. The auth pipe returns { user } which is safe because user is not a protected key.

After-hooks

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)`)
    }
  }
}

Writing Custom Pipes

// 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: [...]
}

Pipe Execution Order

// 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)

Router Middleware (router.use)

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)
When to use — Router middleware is lower-level than pipes. For most use cases, global pipes (createApp({ pipe: [...] })) are the recommended approach. Use router.use() when you need middleware that runs before pipe resolution.