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
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:
/// <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:
// 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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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].
{
"config": {
"APP_NAME": "my-api",
"APP_PORT": "3000",
"NODE_ENV": "development",
"DATABASE_URL": "[REDACTED]",
"JWT_SECRET": "[REDACTED]"
}
}Configuration
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:
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:
- Reactive primitives (
ref,computed,watch) from@forinda/kickjs/reactivity - Middleware that increments reactive counters on each request (phase:
beforeGlobal) - 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:
| State | Meaning |
|---|---|
| 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.
| Tab | What it shows |
|---|---|
| Overview | Three-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. |
| Runtime | Heap / RSS / event-loop p99 / GC stats with sparklines, streamed via /runtime/stream. |
| Memory | Leak-risk panel (heap-growth slope + GC reclaim ratio + heap utilization), heap-snapshot capture button, force-GC button. |
| Topology | Plugin / adapter / contributor / DI-token introspection from /topology. |
| Routes | Method / path / controller / handler / middleware registry. Search input + method filter pills (ALL / GET / POST / PUT / DELETE / PATCH) + paginated (20/page). |
| Metrics | Per-route latency table (avg / p50 / p95 / p99 / max). |
| Container | DI registry — search by token + filter pills (kind: controller / service / repository / other; scope: singleton / transient / request). Expand-row reveals dependency chips, resolve stats, PostConstruct status. |
| Queues | Per-queue cards (waiting / active / completed / failed / delayed / paused) when @forinda/kickjs-queue is mounted. |
| Graph | DI 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: truemounts; the token is sent asx-devtools-tokenheader on every request