Middleware Overview
Introduction
Section titled “Introduction”HTTP middleware applies cross-cutting behavior to requests before and after route handlers run. Examples include creating request context, checking origins, authenticating credentials, limiting request rates, and validating CSRF tokens.
In Xeno.JS, XenoAppBuilder.addMiddlewares() queues and configures the built-in HTTP middleware module. The builder resolves queued modules during build(), creates a middleware pipeline, and connects it to the HTTP dispatcher. Application code configures this stack through MiddlewareConfig; addMiddlewares() is not an API for inserting an arbitrary middleware class into the pipeline.
Middleware configuration belongs to the HTTP boundary. It does not replace route registration, authentication service setup, or application-layer authorization.
Add the Middleware Stack
Section titled “Add the Middleware Stack”Call addMiddlewares() before build(). Pass a setup callback to configure its options:
import { XenoAppBuilder } from '@xeno-js/core'import { withSupabase } from '@xeno-js/supabase'import { ordersRoutes } from './order/order.route'
const app = new XenoAppBuilder()
app.addRoutes(ordersRoutes)app.addAuth( withSupabase((options, config) => { options.url = config.getOrThrow('SUPABASE_URL') options.key = config.getOrThrow('SUPABASE_KEY') }),)app.addMiddlewares((options, config) => { options.cors = { enabled: true, enablePreflight: true, allowOrigins: [config.getOrThrow('CLIENT_URL')], allowHeaders: ['X-Client-Version'], } options.rateLimit = { enabled: true, maxRequests: 100, windowSeconds: 60, }})
const application = await app.build()ordersRoutes represents the application’s NativeRouteDefinition[]. addRoutes() and addAuth() queue their modules; addMiddlewares() queues the middleware module. During build(), Xeno resolves queued modules by priority and constructs the HTTP pipeline. Configure routes, authentication, and middleware before building the application.
The builder queues the middleware module only once. Subsequent calls to addMiddlewares() return the same builder without applying another setup callback. Put the complete middleware configuration in a single call.
Without a callback, addMiddlewares() uses the builder’s defaults. When authentication is not configured, the stack still includes an authentication middleware, but it uses NoAuthGateKeeper and establishes a guest identity rather than authenticating credentials.
Middleware Pipeline
Section titled “Middleware Pipeline”When configured, the HTTP middleware pipeline runs in this order:
RequestContextMiddleware ↓CookieResponseMiddleware (when isSSR is true) ↓AllowOriginMiddleware ↓OptionsMiddleware (when preflight is enabled and routes exist) ↓MethodCheckMiddleware (when routes exist) ↓CORSMiddleware (when cors.enabled is true) ↓RateLimitMiddleware (when rateLimit.enabled is true) ↓AuthenticationMiddleware ↓CsrfCookieMiddleware (when csrf is configured) ↓CsrfMiddleware (when csrf is configured) ↓Route handlerThe pipeline unwinds after the route returns. Response middleware can then append headers or cookies. For example, CORSMiddleware adds CORS response headers after the downstream response is available, and CookieResponseMiddleware appends cookies queued during the request.
Some stages act as request gates and stop processing when a policy fails:
- origin validation returns
403 Forbiddenwhen an origin is denied; - method checking returns
405 Method Not Allowedwhen the request reaches the middleware with a disallowed method; - rate limiting returns
429 Too Many Requestsafter the configured limit is exceeded; - CSRF validation returns
403 Forbiddenwhen a required token is missing, mismatched, or invalid.
For regular HTTP server requests, the route matcher selects a route by path and method before the handler invokes the middleware pipeline. If no route matches, the HTTP adapter returns 404 Not Found; that request does not reach the pipeline.
Middleware Options
Section titled “Middleware Options”addMiddlewares() receives a setup callback with the middleware options and the application’s IConfigurationService:
app.addMiddlewares((options, config) => { options.cors = { allowOrigins: [config.getOrThrow('CLIENT_URL')], }})The following options are available.
CORS options
Section titled “CORS options”| Option | Default | Behavior |
|---|---|---|
cors.enabled |
true |
Adds CORSMiddleware, which adds CORS response headers after the request continues through the pipeline. |
cors.enablePreflight |
true |
Adds OptionsMiddleware when routes exist. It handles browser OPTIONS preflight requests. |
cors.withCredentials |
true |
Sets the default Access-Control-Allow-Credentials value. A route-specific security.withCredentials policy takes precedence when defined. |
cors.allowOrigins |
[] |
Application-level origin allowlist. When empty or omitted, Xeno uses APP_URL, or http://localhost:<PORT> with 3000 as the default port. |
cors.allowHeaders |
Content-Type, Authorization, X-Requested-With, Accept, Origin |
Adds application-specific headers to the list returned by preflight handling. |
cors.allowOrigins and cors.enabled control different behavior. The origin-validation middleware checks requests independently of cors.enabled; turning off CORS response headers does not bypass origin validation.
cors.allowHeaders is used by OptionsMiddleware to build Access-Control-Allow-Headers. Xeno also includes standard headers such as x-csrf-token, Authorization, Content-Type, Origin, request-tracing headers, and other framework headers. It combines the built-in and configured entries and removes duplicates. Add each application-specific request header, such as X-Client-Version, to allowHeaders; Xeno does not dynamically copy the browser’s Access-Control-Request-Headers.
Allowed methods come from the methods registered with addRoutes(), not from a CORS option. For cross-origin browser requests, the allowed origin, requested method, and request headers must all be covered. See CORS Middleware, Allow-Origin Middleware, and OPTIONS Middleware.
Rate-limit options
Section titled “Rate-limit options”| Option | Default | Behavior |
|---|---|---|
rateLimit.enabled |
true |
Enables the request rate limiter. |
rateLimit.maxRequests |
100 |
Maximum requests allowed for a rate-limit key during the window. |
rateLimit.windowSeconds |
60 |
Rate-limit window duration in seconds. |
The rate limiter uses the configured cache. If rate limiting is enabled and no cache service has been registered, Xeno configures its in-memory cache. An in-memory cache is local to one process; multi-instance deployments need a shared cache to share counters. See Rate Limiting.
CSRF options
Section titled “CSRF options”CSRF protection is disabled by default. Supplying options.csrf enables both the cookie-issuing middleware and the state-changing request validator.
| Option | Default | Behavior |
|---|---|---|
csrf.secret |
Required | Server-side secret used to sign and validate user-bound tokens. Keep it private and consistent across application instances. |
csrf.cookieName |
__Host-xeno-csrf |
Cookie that carries the CSRF token. |
csrf.headerName |
x-csrf-token |
Request header that must echo the cookie token. |
csrf.cookieMaxAgeSeconds |
3600 |
CSRF cookie lifetime in seconds. |
csrf.sameSite |
lax |
Cookie SameSite policy: strict, lax, or none. |
The middleware validates POST, PUT, PATCH, and DELETE requests. It requires matching cookie and header values, an authenticated userId, and a valid signature. A route can explicitly opt out with security.withCsrf: false. See CSRF Middleware.
Request and cookie options
Section titled “Request and cookie options”| Option | Default | Behavior |
|---|---|---|
isSSR |
false |
Adds CookieResponseMiddleware to append pending SSR cookies from the request-scoped cookie handler to the response. It does not register a cookie handler itself. |
trustedIpHeader |
undefined |
Selects the header from which the client IP is extracted when the connecting proxy is trusted. The extractor otherwise checks x-forwarded-for, then x-real-ip. |
trustedProxies |
[] |
Exact socket IP addresses of trusted proxies. Xeno uses a forwarded IP only when the connecting socket address matches an entry. |
authCookieName |
sb-access-token |
Cookie name used by the default bearer-token extractor when no custom token extractor is registered. |
Only list proxies you control. trustedProxies currently matches exact addresses; it does not interpret CIDR ranges or '*'. See Request Context Middleware for client IP resolution and Cookie Response Middleware for SSR cookie handling.
Configuration Example
Section titled “Configuration Example”This example shows a typical browser-facing API configuration. Keep secrets in protected application configuration, and replace the example application values with the deployment’s actual origins and routes:
app.addMiddlewares((options, config) => { options.cors = { enabled: true, enablePreflight: true, withCredentials: true, allowOrigins: [config.getOrThrow('CLIENT_URL')], allowHeaders: ['X-Client-Version'], }
options.rateLimit = { enabled: true, maxRequests: 60, windowSeconds: 60, }
options.csrf = { secret: config.getOrThrow('CSRF_SECRET'), cookieName: '__Host-xeno-csrf', headerName: 'x-csrf-token', cookieMaxAgeSeconds: 3600, sameSite: 'lax', }
options.trustedIpHeader = 'x-forwarded-for' options.trustedProxies = ['10.0.0.5']})Set CSRF only when the application uses cookie-based credentials and requires CSRF protection. Set trusted proxy options only when the listed proxy is the trusted network peer that connects to the application.
Common Issues
Section titled “Common Issues”Middleware settings seem to have no effect
Section titled “Middleware settings seem to have no effect”Check that addMiddlewares() runs before build() and that the options are configured in its callback. The builder queues the middleware module once; later calls do not apply another setup callback.
A custom request header fails during browser preflight
Section titled “A custom request header fails during browser preflight”Add the header to cors.allowHeaders. The preflight handler advertises Xeno’s standard headers plus the configured list; it does not reflect arbitrary headers from the incoming preflight request.
Changing cors.enabled does not stop an origin rejection
Section titled “Changing cors.enabled does not stop an origin rejection”This is expected. AllowOriginMiddleware performs request-origin validation independently from CORSMiddleware, which adds response headers. Configure the origin allowlist or route-specific policy rather than relying on cors.enabled.
A route returns 404 instead of a middleware error
Section titled “A route returns 404 instead of a middleware error”The route matcher selects the path and method before the handler enters the middleware pipeline. Register the route with addRoutes() or correct the requested path and method.
All clients behind a proxy share one rate-limit key
Section titled “All clients behind a proxy share one rate-limit key”Check that the connecting proxy’s socket IP exactly matches an entry in trustedProxies and that the proxy supplies the configured client IP header. Do not trust arbitrary forwarded headers from untrusted clients.
Middleware Guides
Section titled “Middleware Guides”- Allow-Method Middleware
- Allow-Origin Middleware
- Cookie Response Middleware
- CORS Middleware
- CSRF Middleware
- OPTIONS Middleware
- Rate Limiting
- Request Context Middleware
Related Documentation
Section titled “Related Documentation”Support Us
Section titled “Support Us”Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section
