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.
Recommended Pipeline Order
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.jszlibintegration 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.