Spraxium logoSpraxium

Security, Guards, and Middleware

Understand the default hardening stack and how to extend it safely. Apply global and route-level guards or middleware with predictable execution order.

Security starts before controllers

HttpServer applies security middleware before any controller handler is invoked. This startup-time wiring gives every endpoint the same baseline policy from the first request without requiring explicit decoration on each route. The security stack runs in a fixed order that cannot be reordered by user code, ensuring that header hardening and body limits are always the first line of defense.

When HTTP is enabled, the runtime wires these protections in this exact sequence:

  1. Optional removal of X-Powered-By and Server headers (hidePoweredBy, defaults to true).
  2. SecurityHeadersMiddleware wrapping Hono secureHeaders (unless headers is set to false).
  3. CorsMiddleware wrapping Hono cors (only when security.cors is configured).
  4. BodyLimitMiddleware wrapping Hono bodyLimit (unless bodyLimit is set to false).
  5. Global middleware instances from @HttpClientModule().
  6. Global guards from @HttpClientModule() through GuardExecutor.
  7. Per-controller and per-route guards from @HttpGuards through GuardExecutor.

After this stack, route-level middleware from @HttpUseMiddleware decorators runs, and finally the handler itself executes. This layered model means your guard decisions happen after baseline hardening, and your middleware decisions happen after security headers are already set.

Default security values

Every security header and limit has a safe default from SECURITY_DEFAULTS. You only need to override values when your deployment requires different behavior. When a subfield is omitted, the default applies. When set to false, that layer is disabled entirely.

Security and middleware configuration

import { defineHttp } from '@spraxium/http';
import { AppModule } from '../src/app.module';

export const httpPlugin = defineHttp({
  enabled: true,
  host: process.env.HTTP_HOST ?? '0.0.0.0',
  port: Number(process.env.HTTP_PORT ?? 3000),
  apiKey: process.env.HTTP_API_KEY ?? '',
  sharding: false,
  security: {
    hidePoweredBy: true,
    headers: {
      frameOptions: 'DENY',
      contentSecurityPolicy: "default-src 'none'",
      referrerPolicy: 'no-referrer',
      hstsMaxAge: 31_536_000,
      hstsIncludeSubDomains: true,
    },
    cors: {
      origins: ['https://admin.example.com'],
      methods: ['GET', 'POST', 'PATCH'],
      allowedHeaders: ['Content-Type', 'Authorization', 'X-Api-Key'],
      credentials: true,
      maxAge: 600,
    },
    bodyLimit: { maxSize: 256_000 },
  },
  module: AppModule,
});

Guard pipeline and the HttpGuard interface

Guards are the authentication and authorization boundary. Every guard implements the HttpGuard interface, which defines a single method: canActivate(ctx: Context): Promise<boolean>. Guards declared in @HttpClientModule() are instantiated via the dependency injection system at startup and execute sequentially through GuardExecutor on every request after the security middleware stack. No manual new call is required when declaring guards in the module.

When GuardExecutor encounters a guard that returns false, it stops execution immediately and sets the response to { ok: false, error: 'Unauthorized' } with status 401. No further guards, middleware, or handlers run. This short-circuit behavior means guard order matters: place your most common rejection case first to avoid running expensive checks on unauthenticated requests.

The 401 response from guard rejection is injected directly, not through the onError handler. This means guard failures are final and cannot be caught or modified by downstream middleware. The response shape matches the same envelope used by all other error responses, so clients do not need special handling for guard rejections.

@HttpGuards for per-controller and per-route protection

@HttpGuards is a class or method decorator that narrows guard scope to a specific controller or individual route. Unlike global guards that protect every endpoint, @HttpGuards targets a single point in the routing surface. The decorator accepts one or more guard classes whose instances are resolved through the same DI system as services and controllers.

Class-level @HttpGuards runs on every route in the controller. Method-level @HttpGuards runs only on that specific handler. When both levels are present, class guards execute first, followed by method guards. This layering means you can protect an entire controller with a shared policy and add extra validation per route without repeating the common guard.

import {
  HttpController,
  HttpGet,
  HttpPost,
  HttpGuards,
  HttpParam,
  HttpBody,
} from '@spraxium/http';
import { AdminGuard } from '../guards/admin.guard';
import { SuperAdminGuard } from '../guards/super-admin.guard';

@HttpGuards(AdminGuard)
@HttpController('/admin')
export class AdminHttpController {
  @HttpGet('/stats')
  getStats() {
    return { ok: true, activeUsers: 42 };
  }

  @HttpGuards(SuperAdminGuard)
  @HttpPost('/reset')
  reset(@HttpParam('confirm') \_confirm: string) {
    return { ok: true };
  }
}

ApiKeyGuard in detail

ApiKeyGuard is the built-in guard shipped with the package. It reads the x-api-key header from the request and compares it against the value provided at construction time. The comparison uses timingSafeEqual from Node's crypto module to prevent timing attacks that could leak information about the key length or prefix.

The guard rejects requests in three scenarios: missing header (returns false immediately), different buffer length (returns false without comparison to avoid the timingSafeEqual requirement for equal lengths), and mismatched values (returns false after constant-time comparison). This layered rejection strategy avoids exposing key length through response timing differences.

ApiKeyGuard is activated automatically when apiKey is set in defineHttp. The runtime creates and registers the guard instance internally so you do not need to declare it in @HttpClientModule(). The guards array is reserved for custom guards that require your own DI-resolved dependencies.

Middleware execution model

The package supports two middleware entry points with different scopes and registration mechanisms.

Global middleware instances are declared in @HttpClientModule() as an array of HttpMiddleware objects. They are registered on the '*' Hono path, which means they run on every request after the security stack but before guard execution. Use global middleware for cross-cutting concerns that apply unconditionally, such as request logging via LoggerMiddleware.

Route-level middleware uses class tokens and the @HttpUseMiddleware decorator. The class token must be mapped to a concrete instance in @HttpClientModule(). RouteBuilder resolves the token during route binding and inserts the middleware handler before the route handler in the Hono chain. Class-level @HttpUseMiddleware runs before method-level @HttpUseMiddleware, and both run after global middleware and guards.

Every middleware implements the HttpMiddleware interface: handle(ctx: Context, next: Next): Promise<undefined | Response>. Calling await next() passes control to the next middleware or handler. Not calling next() terminates the request at that middleware, which is how RateLimitMiddleware rejects over-limit requests.

Built-in middleware reference

Rate limiting strategy

RateLimitMiddleware implements an in-memory sliding window rate limiter. Each client is identified by the x-forwarded-for header value, or 'unknown' when the header is absent. The middleware maintains a Map<string, WindowEntry> where each entry tracks the request count and window start timestamp.

When a request arrives, the middleware checks if the current window has expired. If it has, the entry resets. If the window is still active and the count exceeds max, the middleware responds with { ok: false, error: 'Too many requests' } at status 429 without calling next(). A periodic cleanup interval evicts entries for clients that have not sent requests within the window duration.

To use rate limiting per-route, extend RateLimitMiddleware with your own configuration and register it in middlewareProviders. This pattern keeps the rate limit configuration visible at the middleware class level and avoids magic numbers in decorator arguments.

Proxy trust and IP identification

The rate-limit middleware reads x-forwarded-for directly without verifying the source. In production behind reverse proxies or load balancers, this means you must ensure your proxy infrastructure sets the header correctly. If the header is missing, all requests are grouped under the 'unknown' key, which effectively creates a single global rate-limit bucket. If the header is spoofable because the proxy does not overwrite it, attackers can bypass rate limits by sending fake IP headers.

The SecurityConfig interface includes a trustedProxies field with a proxies array (defaulting to ['127.0.0.1', '::1']). Use this field to document your trusted proxy configuration, and ensure your deployment architecture only allows trusted proxies to set forwarding headers.

Operational hardening checklist

Start with secure defaults

Enable baseline headers and body limits unless a clear requirement forces you to relax them. Default-deny posture is easier to audit than a permissive posture patched later. Override individual headers only when your deployment requires it.

Wire global guards in AppHttpModule

Declare custom guard classes in @HttpClientModule() to protect every endpoint with the same policy. The framework resolves guard dependencies from the DI container, so guards can inject services like AppEnv without any manual wiring. Use apiKey in defineHttp to activate ApiKeyGuard automatically without declaring it in the guards array.

Narrow protection with @HttpGuards

Apply @HttpGuards at the controller or method level for endpoints that require different or additional authorization logic. This keeps the global guard focused on the baseline policy and moves route-specific checks to the point of declaration. Class-level guards execute before method-level guards within the same controller.

Add LoggerMiddleware for observability

Register new LoggerMiddleware() in @HttpClientModule() to get baseline request logging. The middleware logs method, path, status, and duration. Status codes 400 and above are logged at warn level for easier filtering.

Scope rate limits to sensitive endpoints

Extend RateLimitMiddleware with per-route configurations and register through middlewareProviders. Apply via @HttpUseMiddleware on endpoints that handle expensive operations or sensitive mutations rather than applying a single global limit.

Validate proxy and IP trust settings

If rate limiting or audit logging depends on client IP, verify how your reverse proxy forwards x-forwarded-for. Incorrect trust setup can group unrelated users into one bucket or allow malicious clients to spoof their identity.

Security posture is still your design responsibility

The package gives primitives and safe defaults, but final policy decisions must be explicit in your AppModule and deployment architecture. Review security headers, CORS origins, and guard configuration during every deployment review.