Skip to content

DevTools Adapter

The DevTools adapter provides Vue-style reactive introspection for KickJS applications. It exposes debug endpoints that let you inspect routes, DI container state, request metrics, and application health — all powered by the reactivity module.

Quick Start

ts
import { bootstrap } from '@forinda/kickjs'
import { DevToolsAdapter } from '@forinda/kickjs-devtools'

bootstrap({
  modules: [UserModule, ProductModule],
  adapters: [
    DevToolsAdapter({
      enabled: process.env.NODE_ENV !== 'production',
    }),
  ],
})

DevTools endpoints are now available at /_debug/*.

Stripping DevTools from production bundles

The runtime enabled flag above keeps the DevTools adapter inert in prod, but the import of @forinda/kickjs-devtools is still in the bundle — code, dependencies, and all. To drop the package entirely from the production output, gate the import behind the __KICKJS_DEVTOOLS__ build-time flag injected by @forinda/kickjs-vite:

ts
/// <reference types="@forinda/kickjs-vite/globals" />
import { bootstrap } from '@forinda/kickjs'

const adapters = [
  /* prod adapters… */
]

if (__KICKJS_DEVTOOLS__) {
  const { DevToolsAdapter } = await import('@forinda/kickjs-devtools')
  adapters.push(DevToolsAdapter({ basePath: '/_debug' }))
}

bootstrap({ modules: [UserModule, ProductModule], adapters })

Vite/Rollup substitutes __KICKJS_DEVTOOLS__ with true (during vite dev) or false (during vite build) and tree-shakes the unreachable branch — including the dynamic import('@forinda/kickjs-devtools') chunk — out of the prod bundle entirely.

The kickjsVitePlugin() registers the flag by default. Override per build via the env var KICKJS_DEVTOOLS=0|1 or per project via plugin options:

ts
// vite.config.ts
import { kickjsVitePlugin } from '@forinda/kickjs-vite'

export default defineConfig({
  plugins: [
    kickjsVitePlugin({
      // Force-disable for a "no-devtools" build profile
      devtools: { enabled: false },
      // Or rename the global to avoid collisions:
      // devtools: { flagName: '__APP_DEVTOOLS__' },
      // Or skip the plugin entirely:
      // devtools: false,
    }),
  ],
})

Resolution order: explicit enabled option → KICKJS_DEVTOOLS=0|1|true|false env → Vite command ('serve'true, 'build'false).

Endpoints

GET /_debug/routes

Lists all registered routes with their HTTP method, path, controller, handler, and middleware.

json
{
  "routes": [
    {
      "method": "GET",
      "path": "/api/v1/users",
      "controller": "UserController",
      "handler": "getAll",
      "middleware": ["authGuard"]
    },
    {
      "method": "POST",
      "path": "/api/v1/users",
      "controller": "UserController",
      "handler": "create",
      "middleware": ["authGuard", "validate"]
    }
  ]
}

GET /_debug/container

Shows all DI container registrations with their scope and instantiation status.

json
{
  "registrations": [
    { "token": "UserService", "scope": "singleton", "instantiated": true },
    { "token": "ProductService", "scope": "singleton", "instantiated": false }
  ],
  "count": 2
}

GET /_debug/metrics

Live request metrics powered by reactive refs and computed values.

json
{
  "requests": 1542,
  "serverErrors": 3,
  "clientErrors": 28,
  "errorRate": 0.0019,
  "uptimeSeconds": 3600,
  "startedAt": "2026-03-20T10:00:00.000Z",
  "routeLatency": {
    "GET /api/v1/users": {
      "count": 500,
      "totalMs": 2500,
      "minMs": 2,
      "maxMs": 45
    }
  }
}

GET /_debug/health

Deep health check with computed status derived from reactive error rate.

json
{
  "status": "healthy",
  "errorRate": 0.0019,
  "uptime": 3600,
  "adapters": {
    "DevToolsAdapter": "running"
  }
}

Returns 200 when healthy, 503 when degraded (error rate exceeds threshold).

GET /_debug/ws

WebSocket stats when WsAdapter is active. Shows namespaces, connections, message counts, and rooms.

json
{
  "enabled": true,
  "totalConnections": 42,
  "activeConnections": 12,
  "messagesReceived": 1580,
  "messagesSent": 3200,
  "errors": 0,
  "namespaces": {
    "/ws/chat": { "connections": 8, "handlers": 10 },
    "/ws/notifications": { "connections": 4, "handlers": 4 }
  },
  "rooms": {
    "/ws/chat": ["room:general", "room:support"]
  }
}

Returns 404 if no WsAdapter is registered.

GET /_debug/state

Full reactive state snapshot — everything in one endpoint.

json
{
  "reactive": {
    "requestCount": 1542,
    "errorCount": 3,
    "clientErrorCount": 28,
    "errorRate": 0.0019,
    "uptimeSeconds": 3600,
    "startedAt": "2026-03-20T10:00:00.000Z"
  },
  "routes": 12,
  "container": 8,
  "routeLatency": {}
}

GET /_debug/config (opt-in)

Sanitized environment variables. Only variables matching configured prefixes are shown; everything else is [REDACTED].

json
{
  "config": {
    "APP_NAME": "my-api",
    "APP_PORT": "3000",
    "NODE_ENV": "development",
    "DATABASE_URL": "[REDACTED]",
    "JWT_SECRET": "[REDACTED]"
  }
}

Configuration

ts
DevToolsAdapter({
  // Base path for debug endpoints (default: '/_debug')
  basePath: '/_debug',

  // Only enable when true (default: process.env.NODE_ENV !== 'production')
  enabled: process.env.NODE_ENV !== 'production',

  // Expose sanitized env vars at /_debug/config (default: false)
  exposeConfig: true,

  // Env var prefixes to expose (default: ['APP_', 'NODE_ENV'])
  configPrefixes: ['APP_', 'DATABASE_', 'NODE_ENV'],

  // Error rate threshold for health degradation (default: 0.5)
  errorRateThreshold: 0.5,

  // Custom callback when error rate exceeds threshold
  onErrorRateExceeded: (rate) => {
    slackWebhook.send(`Error rate: ${(rate * 100).toFixed(1)}%`)
  },
})

Accessing Reactive State Programmatically

The adapter exposes its reactive state as public properties, so you can compose with it:

ts
const devtools = DevToolsAdapter()

// Read reactive values
console.log(devtools.requestCount.value)
console.log(devtools.errorRate.value)

// Watch for changes
import { watch } from '@forinda/kickjs'

watch(devtools.errorRate, (rate) => {
  if (rate > 0.1) pagerDuty.alert('High error rate')
})

// Subscribe directly
devtools.requestCount.subscribe((newCount) => {
  prometheus.gauge('http_requests_total').set(newCount)
})

How It Works

The DevToolsAdapter uses three layers:

  1. Reactive primitives (ref, computed, watch) from @forinda/kickjs/reactivity
  2. Middleware that increments reactive counters on each request (phase: beforeGlobal)
  3. Express routes at /_debug/* that read reactive state and return JSON

Because the state is reactive, the computed values (error rate, uptime) are always consistent and only recalculate when their dependencies change.

Browser Dashboard

When you visit /_debug in a browser, the DevTools adapter serves a single-page dashboard built with Solid + Tailwind. It connects to the JSON endpoints documented above and adds live UI on top — there's nothing to install client-side.

Connection state

A pill in the global header shows what the dashboard is doing:

StateMeaning
Live (green pulse)Subscribed to /stream SSE — metrics + container changes push in real time
Polling (amber pulse)SSE dropped, falling back to a 5-second /health + /metrics poll
Connecting… (grey)First request hasn't returned yet
Disconnected (red)Teardown — open the dashboard again to reconnect

The trailing Updated HH:MM:SS timestamp is the last successful refresh, so you can tell at a glance the page isn't frozen.

Tabs

Each tab subscribes to a slice of the shared store; nothing owns its own polling loop.

TabWhat it shows
OverviewThree-card landing — Health (status / uptime / error rate / adapters), Metrics (request counts / 5xx / 4xx / started-at), WebSocket (active / total / msgs in+out / namespaces). Default tab on first visit.
RuntimeHeap / RSS / event-loop p99 / GC stats with sparklines, streamed via /runtime/stream.
MemoryLeak-risk panel (heap-growth slope + GC reclaim ratio + heap utilization), heap-snapshot capture button, force-GC button.
TopologyPlugin / adapter / contributor / DI-token introspection from /topology.
RoutesMethod / path / controller / handler / middleware registry. Search input + method filter pills (ALL / GET / POST / PUT / DELETE / PATCH) + paginated (20/page).
MetricsPer-route latency table (avg / p50 / p95 / p99 / max).
ContainerDI registry — search by token + filter pills (kind: controller / service / repository / other; scope: singleton / transient / request). Expand-row reveals dependency chips, resolve stats, PostConstruct status.
QueuesPer-queue cards (waiting / active / completed / failed / delayed / paused) when @forinda/kickjs-queue is mounted.
GraphDI dependency graph kind-grouped (controllers / services / repositories / other) with outgoing-edge arrows. Click any node OR edge target → opens detail modal.

The tab nav scrolls horizontally when there are too many tabs to fit; switching to a tab via localStorage restore scrolls it into view automatically.

Detail modal

Click a token row in Container (or the "View full details" button), or any node in Graph — opens a modal with:

  • Token + kind/scope/status badges
  • Dependencies (outgoing edges) as clickable chips
  • Dependents (incoming edges) as clickable chips
  • Resolve stats (count / first / last / duration)
  • PostConstruct status

Clicking a dependency or dependent navigates to that token's modal in place. The in-modal Back arrow pops one level; Escape or outside-click closes the whole stack.

Beginner-friendly tooltips

Most metric labels carry a small ⓘ icon — hover for a one-line definition. Denser panels (Memory's "Leak risk", PostConstruct status) open a modal with the full explanation, severity bucket boundaries, and worked examples. Wording lives in lib/info.tsx's METRIC_DEFS registry.

Auth gate

If the server runs DevToolsAdapter({ requireToken: true }) and you open the dashboard without a ?token=… query param OR the kickjs_devtools_token cookie, a paste-token modal appears. The token is validated against /health, then persisted to a 30-day cookie. Subsequent visits skip the prompt.

VSCode extension

The same /_debug/* JSON endpoints power the KickJS DevTools VSCode extension — install it, run KickJS: Connect to App… from the palette, and the Activity Bar gets Health / Routes / DI Container tree views without leaving the editor. When the server requires a token, run KickJS: Set DevTools Token… to paste it.

Security

  • DevTools is disabled by default in production (NODE_ENV === 'production')
  • Config endpoint is opt-in and redacts all variables not matching your prefix list
  • Consider adding authentication middleware if exposing in staging environments
  • The browser dashboard's auth gate (above) is the front door for requireToken: true mounts; the token is sent as x-devtools-token header on every request