SpaceNode
Pipes run before your handler. They can validate, authenticate, log, transform, or abort requests. Guards are a type of pipe focused on access control.
Each pipe is a pure function with the same signature as a handler:
// A pipe can:
(request, services) => undefined // continue (no-op)
(request, services) => ({ user }) // merge data into request
(request, services) => ({ after: fn }) // register a post-handler hook
(request, services) => { throw ... } // abort with error
next(). They return data to merge or throw to abort. Simpler, testable, composable.Extracts Bearer token from Authorization header, calls your auth verifier (defineAuth() or app.setAuth()), merges { user } into request.
routes: [
['GET', '/me', 'getProfile', ['auth']],
]
// Handler gets { user } from auth pipe:
export function getProfile(request) {
const { user, send } = request
send(user)
}
Checks request.user.role against allowed roles. Requires auth pipe first.
['DELETE', '/:id', 'delete', ['auth', 'role:admin']]
['POST', '/', 'create', ['auth', 'role:seller,admin']] // multiple roles
Sliding-window rate limiter per IP. N = max requests per minute. Uses in-memory store by default.
['POST', '/login', 'login', ['rateLimit:10', 'dto:loginDto']] // 10 req/min
Response headers — the rate limiter sets these on every response:
| Header | Description |
|---|---|
X-RateLimit-Limit | Configured max requests per window |
X-RateLimit-Remaining | How many requests the client has left |
Retry-After | Seconds until the window resets (only on 429 responses) |
For distributed deployments, plug in a custom store (e.g. Redis):
app.setRateLimitStore({
async hit(key, windowMs) { /* increment and return count */ },
async check(key) { /* return current count */ },
})
Sets CORS headers. Handles OPTIONS preflight automatically.
// Allow all
pipe: ['cors']
// Specific origin
pipe: ['cors:https://mysite.com']
Headers set by CORS:
| Header | Value |
|---|---|
Access-Control-Allow-Origin | Reflected origin, specific origin, or * |
Access-Control-Allow-Methods | GET,POST,PUT,PATCH,DELETE,OPTIONS |
Access-Control-Allow-Headers | Mirrored from client's Access-Control-Request-Headers, or Content-Type,Authorization |
Access-Control-Allow-Credentials | true — allows cookies and auth headers cross-origin |
Access-Control-Max-Age | 86400 (24 hours) — browsers cache the preflight result |
Vary | Origin (when reflecting origin, for proper caching) |
Logs request method, path, status code and timing. Uses after-hooks for accurate timing.
pipe: ['logger']
// Output: 14:30:05 GET /users → 200 (3ms)
Response compression via gzip, brotli, or deflate. Auto-selects best encoding from Accept-Encoding. Responses smaller than 150 bytes are not compressed (overhead would exceed savings).
// Auto-select best encoding
pipe: ['compress']
// Force brotli
pipe: ['compress:br']
// Force gzip
pipe: ['compress:gzip']
Sets common security headers. In strict mode, adds a restrictive Content-Security-Policy and Permissions-Policy.
// Basic security headers
pipe: ['security']
// Strict mode (adds CSP, Permissions-Policy, COOP, CORP)
pipe: ['security:strict']
Headers set by security:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originStrict-Transport-Security: max-age=31536000; includeSubDomainsX-Permitted-Cross-Domain-Policies: noneAdditional headers in strict mode:
Content-Security-Policy — restricts sources to 'self'Permissions-Policy — disables camera, microphone, geolocation, paymentCross-Origin-Opener-Policy: same-originCross-Origin-Resource-Policy: same-originValidates request.body against a named DTO schema defined in a *.dto.js file. If validation fails, throws a ValidationError (400). See DTO Validation for schema syntax.
routes: [
['POST', '/register', 'register', ['dto:registerDto']],
['POST', '/login', 'login', ['dto:loginDto']],
]
import { defineGuard } from 'SpaceNode'
defineGuard('premium', () => (request) => {
if (!request.user?.isPremium) {
throw new HttpError(403, 'Premium subscription required')
}
})
// Use: ['GET', '/data', 'getData', ['auth', 'premium']]
Register guards scoped to a specific app instance:
app.addGuard('tenant', (param) => (request) => {
if (request.headers['x-tenant-id'] !== param) {
throw new HttpError(403, 'Invalid tenant')
}
})
// Use: ['GET', '/data', 'getData', ['tenant:acme']]
When resolving a guard name, SpaceNode checks in order:
app.addGuard())defineGuard())// Global pipes (createApp config.pipe)
// ↓
// Module pipes (module.js pipe: [])
// ↓
// Route pipes (per-route array)
// ↓
// Handler
// ↓
// After-hooks (reverse order, LIFO)