ExisJS

Configuration (exis.config.ts)

ExisJS uses a typed configuration file located at the project root (exis.config.ts or exis.config.js). It controls global server behavior, middleware defaults, file watcher options, cluster concurrency, and runtime environments.


Static Configuration

Export the result of defineConfig() from exis.config.ts for full TypeScript type safety and autocompletion:

exis.config.ts
import { defineConfig } from 'exisjs/config'
export default defineConfig({  port: 4000,  host: '0.0.0.0',
  cors: {    origin: ['https://example.com', 'https://admin.example.com'],    credentials: true,    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']  },
  logger: {    level: 'info',    pretty: true  },
  helmet: {    enabled: true,    xFrameOptions: 'SAMEORIGIN'  },
  bodyLimit: 10 * 1024 * 1024 // 10MB})

Dynamic Phase-Based Configuration

ExisJS supports exporting an asynchronous function that receives the current lifecycle phase and the framework's default configuration:

exis.config.ts
import {  defineConfig,  PHASE_DEVELOPMENT_SERVER,  PHASE_PRODUCTION_SERVER,  PHASE_TEST} from 'exisjs/config'
export default async (phase: string, { defaultConfig }: any) => {  const isDev = phase === PHASE_DEVELOPMENT_SERVER  const isTest = phase === PHASE_TEST
  return defineConfig({    port: Number(process.env.PORT) || 4000,
    logger: isDev      ? { level: 'debug', pretty: true }      : isTest        ? false        : { level: 'info', pretty: false },
    compression: !isDev && !isTest,
    cluster: {      workers: phase === PHASE_PRODUCTION_SERVER ? 'auto' : 1    }  })}

Lifecycle Phase Constants

ConstantValueTriggered By
PHASE_DEVELOPMENT_SERVER'phase-development-server'exisjs dev
PHASE_PRODUCTION_SERVER'phase-production-server'exisjs start
PHASE_PRODUCTION_BUILD'phase-production-build'exisjs build
PHASE_TEST'phase-test'exisjs test / NODE_ENV=test

Environment Variable Management

ExisJS automatically loads and expands .env files in cascade priority:

  1. .env.${NODE_ENV}.local
  2. .env.local (ignored when NODE_ENV=test)
  3. .env.${NODE_ENV}
  4. .env

Type-Safe Validation with tex

To ensure configuration values are valid before starting the HTTP server:

src/config/env.ts
import { tex } from 'exisjs/validator'
export const env = tex  .object({    PORT: tex.number({ coerce: true, default: 4000 }),    NODE_ENV: tex.enum(['development', 'production', 'test'], { default: 'development' }),    DATABASE_URL: tex.string(),    REDIS_URL: tex.string({ optional: true })  })  .parse(process.env)
exis.config.ts
import { defineConfig } from 'exisjs/config'import { env } from './src/config/env'
export default defineConfig({  port: env.PORT,  env: env.NODE_ENV})

Configuration Options Reference

Network & Transport

OptionTypeDefaultDescription
portnumber4000Port number to bind the HTTP server to.
hoststring'0.0.0.0'Network host interface.
server'auto' | 'node' | 'bun' | 'uws''auto'Underlying server adapter engine. 'auto' uses Bun under Bun, otherwise Node. 'uws' needs uWebSockets.js, which is not on npm and not installed with exisjs: npm install https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.71.0.tar.gz. Without it, or on Linux with glibc older than 2.38 (such as the node:24-slim image; use node:24-trixie-slim), the server falls back to Node with a warning.
ssl{ key: string | Buffer, cert: string | Buffer }undefinedSSL certificate credentials for native HTTPS.
http2booleantrueEnables HTTP/2 (active when SSL is configured).
redirectHttpboolean | numberfalseAutomatically redirects plaintext HTTP traffic to HTTPS.
trustProxyboolean | numberfalseTrust proxy headers. true trusts every hop (including CDN headers); a number N trusts the N closest hops, counting the socket peer.
keepAliveboolean | KeepAliveConfigfalseConfigures socket keep-alive (timeoutMs, headersTimeoutMs, maxRequests).

Middleware Defaults & Security

OptionTypeDefaultDescription
corsboolean | CorsConfig{ origin: '*' }Global CORS policy. Can be overridden in boundaries or routes. credentials: true requires explicit origins: with origin: '*' it is ignored and a warning is logged, because browsers reject that combination.
helmetboolean | HelmetConfig{ enabled: true }Security headers, including a same-origin Content-Security-Policy. contentSecurityPolicy: false sends no CSP; a string replaces the default.
authorizationAuthorizationConfig{}defaultPolicy: 'authenticated' closes controller routes that declare no rule unless they are @Public(). superUser: (user) => boolean lets matching users pass every @Roles / @Permissions rule.
errors{ legacyFields?: boolean }{}legacyFields: true adds the pre-0.9 duplicate fields to error bodies. Deprecated.
compressionbooleanfalseGzip/Brotli HTTP response compression.
bodyLimitnumber10485760 (10MB)Maximum request payload body size in bytes.
env'development' | 'production' | 'test'NODE_ENV, else 'production'Only development sends error messages and stack traces to clients and loads routes lazily. exisjs dev sets it; a server started with plain node or tsx and no NODE_ENV runs as production.
blockSuspiciousProbesboolean | BlockProbesOptionsfalseBlocks known vulnerability scanners and malicious probes (.php, wp-admin).
etagbooleanfalseWeak ETags on successful GET/HEAD responses, with 304 Not Modified for a matching If-None-Match. See Caching & Conditional Requests.
transformResponseboolean | FunctionfalseStandardizes handler response payload envelopes.

Logging & Diagnostics

OptionTypeDefaultDescription
loggerboolean | LoggerConfigfalsePino logger settings (level, pretty, redact).
debugRoutingbooleanfalseOutputs detailed route resolution logs per request in development.
asyncContextbooleanfalseEnables AsyncLocalStorage context tracking (getContext()).
docs / swaggerboolean | SwaggerConfig{ enabled: false }OpenAPI 3.1 specification and Swagger UI endpoint generation. Served in every environment when enabled; the project template uses enabled: process.env.NODE_ENV !== 'production' so published API docs do not help attackers.
healthcheckboolean | HealthCheckConfig{ enabled: false }path (/_health): liveness, runs checks. readyPath (/_ready, false to disable): readiness, 503 unless checks pass, registered databases are healthy and the app is not shutting down. drainMs (5000 in production, else 0): on shutdown, how long /_ready answers 503 before the server stops accepting connections.
metricsboolean | MetricsConfig{ enabled: false }Prometheus metrics endpoint (path: '/metrics'). See Prometheus Metrics.
telemetryboolean | TelemetryConfig{ enabled: false }OpenTelemetry tracing and metrics export.

Concurrency & Clustering

exis.config.ts
import { defineConfig } from 'exisjs/config'
export default defineConfig({  cluster: {    // 1 (single-threaded), 'safe' (up to 2 cores), 'auto'/'max' (all available CPU cores)    workers: 'auto'  }})

Limiting concurrent requests (queue)

queue caps how many requests the app handles at once, so a traffic spike slows down instead of exhausting memory or the database pool:

exis.config.ts
export default defineConfig({  queue: {    maxConcurrent: 500, // handled at once (default 1000)    maxQueue: 200, // may wait for a free slot (default 500)    timeoutMs: 10_000, // longest wait (default 10 s)  },})

A request over the limit waits in this process; when maxQueue is full, or the wait exceeds timeoutMs, it gets 503 Service Unavailable. A client that disconnects frees its place. The limit is per process (per worker in cluster mode).

The only driver is 'memory'. Any other driver, unknown keys and invalid numbers stop the server at start-up with a message naming the option. ExisJS has no shared or durable queue yet, so a setting that pretended to provide one would be silently ignored.


File Watcher & Development Configuration

The development server (exisjs dev) provides file watching with debounce and ignore rules:

exis.config.ts
import { defineConfig } from 'exisjs/config'
export default defineConfig({  watch: {    paths: ['src', '.env'],    ignore: [/tests\//, 'temp/**'],    extensions: ['.ts', '.tsx', '.js', '.json'],    debounceMs: 250  },
  dev: {    usePolling: false, // Set to true for network mounts or WSL    interval: 100,    lazyRoutes: true // false loads every route at startup, as in production  }})

Automatically Excluded Artifacts

ExisJS automatically excludes build output and temp folders from triggering watcher reloads:

  • Folders: node_modules, .git, .exis, dist, build, out, .turbo, .next, .cache, logs, tmp, temp, tests, coverage.
  • Files: .log, .sqlite, .db, .png, .jpg, .svg, package-lock.json, *.d.ts.

Prometheus Metrics

Set metrics: { enabled: true } (or metrics: true) to serve request and process metrics in the Prometheus text format at GET /metrics (change it with path).

exis.config.ts
export default defineConfig({  metrics: { enabled: true, path: '/metrics' },})
MetricTypeLabels
http_requests_totalcountermethod, route, status
http_request_duration_secondshistogram (5 ms to 10 s)method, route
http_requests_in_flightgauge
process_resident_memory_bytes, nodejs_heap_used_bytesgaugeworker in cluster mode
nodejs_eventloop_lag_p99_secondsgaugeworker in cluster mode
process_start_time_secondsgaugeworker in cluster mode
  • route is the route pattern (/users/:id), never the requested URL, so the number of series stays bounded. Requests that match no route are counted under (unmatched).
  • Status 499 counts requests whose client disconnected before the response finished.
  • Cluster mode: a scrape can reach any worker; that worker collects the numbers of every worker through the primary process, so the request counters are always the whole server's. Memory and event-loop lag are reported per worker.
  • Keep the endpoint private. It lists every route of the API. Expose it only on an internal network, or block it at the proxy for public traffic.

Plugin Integration

Plugins extend framework capabilities by intercepting server configuration or registering global routes:

exis.config.ts
import { defineConfig } from 'exisjs/config'import { defineModule } from 'exisjs/module'
const metricsPlugin = defineModule({  name: 'custom-metrics',  routes: (app) => {    app.get('/metrics/live', (req, res) => {      res.json({ memory: process.memoryUsage() })    })  }})
export default defineConfig({  plugins: [metricsPlugin]})

Graceful Process Termination

ExisJS automatically listens for SIGTERM and SIGINT signals in containerized environments:

  1. Closes the HTTP server listener to stop accepting incoming traffic.
  2. Closes idle keep-alive sockets and waits for in-flight requests to finish (up to the shutdown timeout).
  3. Runs shutdown hooks in order: app.onShutdown() hooks (including onDestroy / onApplicationShutdown(signal) on injectable services), then onClose hooks.
  4. Disconnects every database registered with registerDatabase(), once each.
  5. Stops cron jobs and exits.

Use onClose for resources ExisJS does not manage, such as browser sessions, queues or worker threads:

src/http/server.ts
import { exis } from 'exisjs'import { scraper } from '../workers/scraper'
export default exis({  async onStart(app) {    // Receives the signal name ('SIGTERM', 'SIGINT') when one triggered the shutdown    app.onShutdown(async (signal) => {      app.log.info({ signal }, 'stopping background workers')      await scraper.stop()    })  },  async onClose(app) {    await scraper.closeBrowser()  },})

Databases registered with registerDatabase() are disconnected automatically after requests have drained. Do not also call disconnect() on them in a hook. Each hook has a time limit (half the shutdown timeout, at least 1 second), so a hook that hangs cannot block the process from exiting.