Overview
Devtools serve an inspector from your running application: the DI container, the route table, request waterfalls, logs, storage, resolved config, a bootstrap profile and an audit. It is one self-contained HTML page served by the app itself, with no outbound traffic and no separate process.
Installation
$ pnpm add -D @vercube/devtools
$ npm install --save-dev @vercube/devtools
$ bun install --dev @vercube/devtools
Register the plugin in vercube.config.ts and open http://localhost:3000/_devtools.
import { defineConfig } from '@vercube/core';
import { DevtoolsPlugin } from '@vercube/devtools';
export default defineConfig({
plugins: [DevtoolsPlugin],
});
app.addPlugin(DevtoolsPlugin) inside createApp({ setup }) also works, but attaches after the container is built, so the bootstrap profile will be incomplete.Panels are in the left rail, with number shortcuts 1 to 9. The active panel is written to the URL hash, so /_devtools#requests is shareable. The buttons at the bottom pause the live stream, download a JSON snapshot of everything, and switch the theme.
Overview
Answers "what am I connected to and is it healthy": name, version, mode, runtime, uptime, counts of routes, services and plugins, live throughput, CPU, heap, event loop delay and active handles. Every summary cell opens the panel behind it.
Handy when several apps run on neighbouring ports and you are about to debug the wrong one.
Requests
Answers "where did the time go". Every request is recorded with the status the client observed, and opening one gives a waterfall: a span per middleware, one for the handler, and the framework time left over for routing, argument resolution and serialization. The log lines written during that request, the bodies and the headers are below it.
Bodies are read from a clone of the message, so recording never touches the stream your app consumes. Headers such as authorization and cookie are redacted, but bodies are not, so on an app that posts secrets:
plugins: [withPluginOptions(DevtoolsPlugin, { captureBodies: false })],
Logs
Everything written through the Logger service, filterable by level and stamped with the request that produced it. The search matches structured context, not just the message, so given:
this.logger.info('order created', { orderId, userId });
typing an order id finds that line, and the request id next to it takes you to the call that wrote it. A bare console.log bypasses the Logger service and never shows up here.
Storage
Keys held by every @vercube/storage mount, with the value behind a key on click, plus the defaults @vercube/cache runs with. Read-only: it lists and previews, it never writes.
The usual use is confirming that a @Cache({ maxAge: 300 }) entry is really in the store, under the key you expect, with the value you expect. Values under credential-looking keys are never read back.
Routes
The route table as the router sees it, grouped by controller. Opening a route shows its decorated arguments, whether each one is validated, and the middleware chain in execution order with phases and priorities, which is the fastest answer to "why is this middleware running here" and "why does this path 404".
An argument marked unvalidated means the payload reaches the handler unchecked:
@Post('/orders')
public create(@Body({ validationSchema: OrderSchema }) body: Order) {}
Graph
The container drawn as a graph: nodes by role, arrows from a dependent to its dependency, hollow nodes for bindings nothing has resolved, rings around dependency cycles. Selecting a service lists what it injects and what injects it, both clickable, which is the blast radius to check before changing a constructor.
An @Inject of a key nothing binds is marked unbound here before it throws at runtime. Inspecting never instantiates anything.
Config
The configuration the framework actually resolved, after defaults, files and environment were merged, flattened to dotted paths such as server.port. Search matches paths and values, so a port number finds whichever setting holds it.
Values whose key reads like a credential are replaced with <redacted>. The rule is name-based, so a secret inside a connection string is still shown.
Bootstrap
A flamegraph of container construction: frame width is total time, nesting is injection depth. Below it, services ranked by self time, which excludes the time spent building their dependencies and is therefore the list worth acting on.
The typical finding is a constructor doing I/O at boot. Profiling starts in the plugin's configure phase and stops on the first request, so the profile always describes startup.
Audit
Rules over the container, the router, the bootstrap profile and recorded traffic, scored 0-100:
| Rule | Severity | What it catches |
|---|---|---|
di/unbound-dependency | error | @Inject of a key nothing binds, so resolution will throw |
router/duplicate-route | error | The same method and path registered twice |
runtime/server-errors | error | Recorded requests that returned 5xx |
di/circular-dependency | warning | Dependency loops in the container |
validation/missing-schema | warning | @Body() or @QueryParams() reaching a handler unvalidated |
bootstrap/slow-service | warning | Constructors that dominate startup |
runtime/slow-requests | warning | Requests over 500ms |
di/unbound-optional-dependency | info | @InjectOptional that will always resolve to null |
di/unused-service | info | Bindings nothing has ever resolved |
Runtime rules only see what devtools recorded, so drive some traffic before trusting them.
Snapshot and JSON
Snapshot downloads one JSON file with the overview, graph, routes, bootstrap profile, audit, config, requests and logs: the exact state the UI is showing, and the right thing to attach to a bug report.
Everything the UI reads is plain JSON under the mount, so the same data works outside the browser:
curl -s localhost:3000/_devtools/api/audit | jq '{score, errors: .counts.error}'
curl -s localhost:3000/_devtools/api/routes | jq 'length'
Overhead
Three hooks, confined to development: one on container construction, one pass over the route table on the first request, and a wrap of the HTTP entry point for status and duration. Bootstrap profiling stops on the first request and the buffers are bounded, so a long-running dev server does not grow without limit.
Devtools are off in production builds unless you enable them explicitly, and then only behind a token. See Configuration.