Docs/Middlewarev1.0.0-rc.1

Built-in Middleware

Package: @tagna/udiot v1.0.0-rc.1 Source: src/middleware/built-in/ Import:

import {
  authMiddleware, jwtAuth, apiKeyAuth, sessionAuth,
  validationMiddleware,
  loggingMiddleware,
  rateLimitingMiddleware,
  corsMiddleware,
  compressionMiddleware,
  errorHandlerMiddleware,
  cachingMiddleware
} from '@tagna/udiot';

All built-in middleware operate on HttpContext and return Middleware<HttpContext> objects.


corsMiddleware          // Must run first — sets headers including preflight 204
  → errorHandlerMiddleware  // Wraps everything below so errors are formatted
    → authMiddleware         // Identifies the user before rate limiting
      → rateLimitingMiddleware // Rate-limit authenticated user or IP
        → loggingMiddleware      // Log request + timing (wraps next())
          → cachingMiddleware     // Return cached response early if available
            → validationMiddleware  // Reject invalid input before business logic
              → [business logic]
                → compressionMiddleware  // Compress after response is assembled

Place errorHandlerMiddleware early (just after CORS) so its try/catch wraps all downstream middleware, including auth, rate limiting, and business logic.


Auth Middleware

Source: src/middleware/built-in/auth.ts

Verifies authentication credentials and sets ctx.user on success. On failure with optional: false (default), short-circuits with HTTP 401.

The JWT strategy verifies signatures with an explicit algorithm allowlist: HMAC (HS256, the default, plus HS384/HS512) with secret, or RSA (RS256/RS384/RS512) and ECDSA (ES256/ES384/ES512) with publicKey. One middleware accepts either HMAC or public-key algorithms, never both, so a public key can never be replayed as an HMAC secret. It validates numeric time claims when present, checks issuer/audience when configured, and rejects tokens without an exp claim unless requireExp: false is set. It does not fetch JWKS documents; pass the key your identity provider publishes. Production JWT secrets must be at least 32 characters and must not be a default. API-key and session strategies require real validation callbacks; header/cookie presence alone is not authentication.

authMiddleware(options)

function authMiddleware(options: AuthOptions): Middleware<HttpContext>

AuthOptions

Option Type Default Description
strategy 'jwt' | 'apikey' | 'session' | 'custom' required Authentication strategy.
secret string — JWT HMAC secret. Required for HS* algorithms.
publicKey string | KeyObject — PEM or key object for RS*/ES* algorithms.
algorithms JwtAlgorithm[] ['HS256'], or ['RS256'] with only publicKey Explicit accepted algorithms, all HMAC or all public-key.
requireExp boolean true Reject tokens without an exp claim.
issuer / audience string — Expected claims; enforce only when configured.
clockTolerance number 0 Allowed clock skew in seconds.
validateApiKey function — Required API-key validator; return a verified principal or falsy.
validateSessionFn function — Required session validator; check session validity/revocation.
allowApiKeyInQuery boolean false Query keys are disabled by default; URLs can leak credentials.
validateFn (ctx: HttpContext) => Promise<any> | any — Custom validation function. Required when strategy: 'custom'. Returns the user object or null.
optional boolean false When true, missing/invalid credentials are tolerated; ctx.user remains undefined.
header string 'authorization' Header name to read the token from.
cookieName string 'session' Cookie name for session strategy.
import { authMiddleware } from '@tagna/udiot';

// JWT
pipeline.use(authMiddleware({
  strategy: 'jwt',
  secret: process.env.JWT_SECRET!
}));

// JWT signed by an identity provider (RS256)
pipeline.use(authMiddleware({
  strategy: 'jwt',
  publicKey: process.env.IDP_PUBLIC_KEY_PEM!,
  algorithms: ['RS256'],
  issuer: 'https://login.example.com/',
  audience: 'api'
}));

// Custom validator
pipeline.use(authMiddleware({
  strategy: 'custom',
  validateFn: async (ctx) => {
    const token = ctx.headers['x-api-token'];
    return token ? await db.validateToken(token) : null;
  },
  optional: true
}));

Convenience factories

// JWT — shorthand for authMiddleware({ strategy: 'jwt', secret, ...options })
function jwtAuth(secret: string, options?: Partial<AuthOptions>): Middleware<HttpContext>

// API key — shorthand for authMiddleware({ strategy: 'apikey', ...options })
function apiKeyAuth(options?: Partial<AuthOptions>): Middleware<HttpContext>

// Session cookie — shorthand for authMiddleware({ strategy: 'session', cookieName, ...options })
function sessionAuth(cookieName?: string, options?: Partial<AuthOptions>): Middleware<HttpContext>
import { jwtAuth, apiKeyAuth, sessionAuth } from '@tagna/udiot';

pipeline.use(jwtAuth(process.env.JWT_SECRET!));
// Application-owned validators must authenticate the supplied credential.
pipeline.use(apiKeyAuth({ header: 'x-api-key', validateApiKey: verifyApiKey }));
pipeline.use(sessionAuth('auth_session', { validateSessionFn: verifySession }));

Validation Middleware

Source: src/middleware/built-in/validation.ts

Validates ctx.body, ctx.query, ctx.params, and/or ctx.headers against declared schemas. On validation failure, short-circuits with HTTP 400 and a structured error list.

validationMiddleware(options)

function validationMiddleware(options: ValidationOptions): Middleware<HttpContext>

ValidationOptions

Option Type Description
body Record<string, ValidationSchema> Schema for ctx.body fields.
query Record<string, ValidationSchema> Schema for ctx.query fields.
params Record<string, ValidationSchema> Schema for ctx.params fields.
headers Record<string, ValidationSchema> Schema for ctx.headers fields.
abortEarly boolean Default true. Stop at first validation error.
stripUnknown boolean Default false. Remove unknown fields from body/query before passing downstream.

ValidationSchema

Field Type Description
type 'string' | 'number' | 'boolean' | 'object' | 'array' | 'email' | 'url' | 'uuid' Expected type.
required boolean Field must be present and non-empty.
min number For strings: minimum character length. For numbers: minimum value.
max number For strings: maximum character length. For numbers: maximum value.
pattern RegExp | string Regular expression the string must match.
enum any[] Value must be one of these.
items ValidationSchema Schema for array elements (when type: 'array').
properties Record<string, ValidationSchema> Nested object schema (when type: 'object').
custom (value: any) => boolean | string Returns true for valid, or an error message string for invalid.
import { validationMiddleware } from '@tagna/udiot';

pipeline.use(validationMiddleware({
  body: {
    email:    { type: 'email', required: true },
    name:     { type: 'string', required: true, min: 2, max: 50 },
    age:      { type: 'number', min: 18, max: 120 },
    role:     { type: 'string', enum: ['admin', 'user', 'guest'] },
    address: {
      type: 'object',
      properties: {
        street: { type: 'string', required: true },
        city:   { type: 'string', required: true }
      }
    }
  },
  query: {
    page:  { type: 'number', min: 1 },
    limit: { type: 'number', min: 1, max: 100 }
  },
  abortEarly: false // collect all errors
}));

Pre-built schemas (commonSchemas):

import { validationMiddleware, commonSchemas } from '@tagna/udiot';

pipeline.use(validationMiddleware({
  body: {
    email:    commonSchemas.email,
    password: commonSchemas.password,   // min 8 chars, upper+lower+digit
    page:     commonSchemas.positiveInteger
  }
}));

Error response shape (HTTP 400):

{
  "error": "Validation failed",
  "errors": [
    { "field": "body.email", "message": "Must be a valid email", "type": "type", "value": "notanemail" },
    { "field": "body.name", "message": "Field is required", "type": "required", "value": null }
  ]
}

Logging Middleware

Source: src/middleware/built-in/logging.ts

Structured request/response logging with OpenTelemetry tracing integration. Assigns requestId, traceId, and spanId to ctx.metadata. Wraps next() so it captures both pre-request and post-response log lines.

loggingMiddleware(options?)

function loggingMiddleware(options?: LoggingOptions): Middleware<HttpContext>

LoggingOptions

Option Type Default Description
level 'debug' | 'info' | 'warn' | 'error' 'info' Log level for request/response entries.
logBody boolean false Include request body in the log entry (respects maxBodySize).
logHeaders boolean false Include request headers (redacted fields are masked).
logResponse boolean false Include the response result.
maxBodySize number 10000 Maximum body size in bytes to log. Larger bodies are replaced with a size note.
logger (message: any) => void Internal logger Custom log sink function.
redactFields string[] ['password', 'token', 'authorization', 'secret'] Field names to mask in logged objects. Case-insensitive substring match.
import { loggingMiddleware } from '@tagna/udiot';

pipeline.use(loggingMiddleware({
  level: 'info',
  logBody: true,
  logHeaders: false,
  redactFields: ['password', 'token', 'authorization', 'secret', 'creditCard']
}));

Metadata written to ctx.metadata:

Key Value
requestId UUID for this request
traceId Distributed trace ID (read from x-trace-id header if present)
spanId Span ID for this middleware execution

Rate Limiting Middleware

Source: src/middleware/built-in/rate-limiting.ts

Fixed-window rate limiter backed by an in-process Map. On limit exceeded, short-circuits with HTTP 429 and a Retry-After header.

rateLimitingMiddleware(options?)

function rateLimitingMiddleware(options?: RateLimitOptions): Middleware<HttpContext>

RateLimitOptions

Option Type Default Description
maxRequests number 100 Maximum requests allowed per window per key.
windowMs number 60000 Window duration in milliseconds.
keyGenerator (ctx: HttpContext) => string ctx.request.ip or 'default' Function that returns the rate-limit key for a request.
message string 'Too many requests, please try again later' Message included in the 429 response body.
import { rateLimitingMiddleware } from '@tagna/udiot';

// 50 requests per minute per IP
pipeline.use(rateLimitingMiddleware({
  maxRequests: 50,
  windowMs: 60 * 1000
}));

// Per-user rate limiting (requires auth middleware to run first)
pipeline.use(rateLimitingMiddleware({
  maxRequests: 1000,
  windowMs: 60 * 60 * 1000, // 1 hour
  keyGenerator: (ctx) => String(ctx.user?.id ?? ctx.request.ip ?? 'anon')
}));

Shared deployments must supply a RateLimitStore whose increment(key, windowMs) atomically reserves the count and returns { count, resetTime } (epoch milliseconds). A separate get/set sequence is not equivalent. Other store methods are get, set and delete. In-memory options include maxEntries (default 100,000) and sweepIntervalMs. Configure proxy trust before relying on request IP; never use untrusted forwarding headers directly as identity.

CORS Middleware

Source: src/middleware/built-in/cors.ts

Sets Cross-Origin Resource Sharing headers on every response. Handles OPTIONS preflight requests automatically (returns HTTP 204 and short-circuits).

corsMiddleware(options?)

function corsMiddleware(options?: CORSOptions): Middleware<HttpContext>

CORSOptions

Option Type Default Description
origin string | string[] | ((origin: string) => boolean) '*' Allowed origin(s). Pass a function for dynamic allow-listing.
methods string[] ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'] Allowed HTTP methods.
allowedHeaders string[] ['Content-Type', 'Authorization'] Headers the browser is permitted to send.
exposedHeaders string[] [] Response headers the browser is permitted to read.
credentials boolean false Set Access-Control-Allow-Credentials: true.
maxAge number 86400 Preflight cache duration in seconds (Access-Control-Max-Age).
import { corsMiddleware } from '@tagna/udiot';

pipeline.use(corsMiddleware({
  origin: ['https://app.example.com', 'https://admin.example.com'],
  credentials: true,
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Api-Key'],
  exposedHeaders: ['X-Request-Id']
}));

Compression Middleware

Source: src/middleware/built-in/compression.ts

Sets Content-Encoding response headers for gzip, deflate, or brotli based on the Accept-Encoding request header. Only compresses responses above the threshold size. Runs its logic after await next() so it inspects the assembled response.

Note: The built-in implementation sets the encoding header and records the compression type in ctx.metadata. Actual byte compression requires a Node.js zlib integration layer in the host application.

compressionMiddleware(options?)

function compressionMiddleware(options?: CompressionOptions): Middleware<HttpContext>

CompressionOptions

Option Type Default Description
threshold number 1024 Minimum response size in bytes to compress.
level number 6 Compression level (1–9).
filter (ctx: HttpContext) => boolean Compress JSON/text/HTML/CSS/JS Custom function to decide whether to compress.
import { compressionMiddleware } from '@tagna/udiot';

pipeline.use(compressionMiddleware({
  threshold: 2048,
  level: 7
}));

Error Handler Middleware

Source: src/middleware/built-in/error-handler.ts

Catches any error thrown by downstream middleware, logs it, and converts it to a structured JSON error response. Place it early in the pipeline so its try/catch wraps all downstream middleware.

errorHandlerMiddleware(options?)

function errorHandlerMiddleware(options?: ErrorHandlerOptions): Middleware<HttpContext>

ErrorHandlerOptions

Option Type Default Description
includeStack boolean process.env.NODE_ENV === 'development' Include stack field in error response.
logger (error: Error, ctx: HttpContext) => void Internal logger Custom error logging function.
transform (error: Error, ctx: HttpContext) => any undefined Custom error-to-response transformer. When provided, replaces the default response format.

The middleware reads err.statusCode or err.status to determine the HTTP status code; falls back to 500.

import { errorHandlerMiddleware } from '@tagna/udiot';

pipeline.use(errorHandlerMiddleware({
  includeStack: false,
  transform: (error, _ctx) => ({
    code: (error as any).code ?? 'INTERNAL_ERROR',
    message: error.message,
    timestamp: new Date().toISOString()
  })
}));

Default error response shape (HTTP 500 unless err.statusCode/err.status is set):

{
  "error": "Error",
  "message": "Something went wrong"
}

Caching Middleware

Source: src/middleware/built-in/caching.ts

In-memory response cache backed by a closure-scoped Map. On a cache hit, short-circuits and returns the cached result without calling next(). Only caches HTTP methods listed in cacheMethods (default: GET). Sweeps expired entries when the cache grows beyond 1000 items; this is not a hard capacity bound.

Scope warning: this basic pipeline cache does not apply all of the server adapter's private-response protections. Do not put it in front of personalized or tenant-sensitive handlers by default. The key alone does not enforce Cache-Control, cookie or authorization policy. For Express responses prefer expressCacheMiddleware and review its policy in the server guide.

cachingMiddleware(options?)

function cachingMiddleware(options?: CachingOptions): Middleware<HttpContext>

CachingOptions

Option Type Default Description
ttl number 60000 Time-to-live in milliseconds.
keyGenerator (ctx: HttpContext) => string METHOD:path:query Cache key function.
cacheMethods string[] ['GET'] HTTP methods to cache.

Sets ctx.metadata.set('cache', 'hit') on a cache hit, 'miss' on a miss.

import { cachingMiddleware } from '@tagna/udiot';

pipeline.use(cachingMiddleware({
  ttl: 5 * 60 * 1000, // 5 minutes
  cacheMethods: ['GET'],
  keyGenerator: (ctx) =>
    `${ctx.request.method}:${ctx.request.path}:${ctx.user?.id ?? 'anon'}`
}));

Application composition

Use the production composition recipe for Express applications. Keep built-in pipeline middleware and Express handlers separate; they are not interchangeable function signatures.

Do not add the basic cache or placeholder compression middleware to a production pipeline merely because the exports exist. The compression implementation sets headers without compressing bytes; use a real compression integration and never send a compression header for uncompressed output. Use an explicit CORS allowlist when credentials are enabled. Authenticate before user-specific limits, validate before business side effects, and inspect the pipeline result envelope.

From the canonical UDIOT public documentation.Try it in the playground ↗