The dumpio-client package (Node ≥ 16, CommonJS and ESM) sends faithful, typed value dumps and structured messages to Dumpio over HTTP. It serializes plain objects and class instances (by constructor name), Map/Set/Date/RegExp/Error/typed arrays, bigint, and functions into the language-agnostic value tree, breaks cycles with ref nodes, and captures the calling file:line automatically. ORM models render as their loaded columns rather than raw internals (Sequelize via dataValues, Mongoose via toObject() — only already-loaded data, no lazy query triggered). Middleware is provided for Express, Koa, and Fastify.
npm install dumpio-clientEvery helper is fire-and-forget and never throws into your app — if the viewer isn't running, the dump is dropped silently. The functions return a Promise that you usually don't need to await.
Why send dumps to Dumpio instead of console.log
- It doesn't flood stdout. The dump travels over the network to a separate window, so you don't have to spew objects into a production server's log or a test runner's output.
- It preserves types and structure. Instead of
[object Object]you see a typed tree — classes,Map/Set,bigint, cycles viaref. - It sorts things for you. Color flags, labels, and channels separate output from different places.
- It won't crash the process. Everything is wrapped so a send error (e.g. when the viewer isn't running) can never leak into your app.
For the full list of dump types and how Dumpio renders them, see Dump types; if nothing shows up, start with Troubleshooting.
Quick start
const { dumpio, dd, configure } = require('dumpio-client')// ESM: import { dumpio, dd, configure } from 'dumpio-client' configure({ host: 'localhost', port: 21234 }) // optional — these are the defaults dumpio(user) // dump a variabledumpio(user, { label: 'user', flag: 'green' }) // with a label and flagdd(a, b) // dump every argument, wait for the send, and exit the processWhich entry point should you use?
| I want to… | Use |
|---|---|
| Quickly dump a value | dumpio(x) |
| Add a flag / label / channel | dumpio(x, { label, flag, channel }) |
| Dump and stop the process | dd(x) |
| Send something other than a variable (exception, query, HTTP…) | typed helpers dumpioException(), dumpioQuery(), … |
| Wait for sends to finish before a script exits | await flush() |
Configuration
Defaults are read from the environment, or you can set them via configure({...}):
| Option | Env var | Default |
|---|---|---|
host |
DUMPIO_HOST |
localhost |
port |
DUMPIO_PORT |
21234 |
path |
– | /dumps |
token |
DUMPIO_TOKEN |
"" |
timeoutMs |
– | 1500 |
enabled |
DUMPIO_DISABLE (set it to turn off) |
true |
maxDepth |
– | 6 |
maxItems |
– | 100 |
maxString |
– | 2000 |
const { config } = configure({ host: '127.0.0.1', port: 21234, token: 'Dio-…' })The token is sent as the X-Dumpio-Token header. configure() returns the resulting config (you can also reach the exported config object). The SDK speaks HTTP only (no TCP).
Core functions
| Function | Purpose |
|---|---|
dumpio(value, options?): Promise<void> |
Send a single variable dump; options = { label?, flag?, channel?, timestamp? } |
dd(...values): void |
Dump every argument, wait for the send (flush), and exit the process |
configure(options): DumpioConfig |
Override config and return the result |
serialize(value, limits?): VarNode |
Return the typed tree without sending (useful for custom envelopes) |
flush(): Promise<void> |
Wait until all in-flight dumps have been sent (always resolves) |
flush() is handy before a short script exits, so the last dumps make it out:
await Promise.all([dumpio(a), dumpioQuery(sql, binds)])await flush()Fluent builder & flood control
dio(value) returns a chainable builder that mirrors the PHP client. It ships on
.send() or automatically at the end of the current tick (JS has no destructors),
so .send() is optional.
const { dio, ddio, stopwatch } = require('dumpio-client') dio(user).red().label('user').channel('auth') // auto-sendsdio(payload).green().send() // explicit dio(state).when(debug) // send only if truthy (.unless() is the opposite) // Keep loops from flooding the viewer (keyed by call site, or a shared name):for (const row of rows) { dio(row).once() // only the first iteration dio(row).limit(5) // at most 5 dio(row).count('rows') // one live-updating "×N" entry} ddio(a, b) // dump every argument, flush, then exit the processColors: red() yellow() blue() gray() purple() pink() green() (or flag('…')),
plus label(), channel(), when()/unless(), once()/limit(n)/count(name?), send().
Display modifiers
These four don't change what you send, only how the viewer shows the entry:
dio(config).expand() // arrives fully open instead of two levels deepdio(hugePayload).collapse() // arrives closeddio(report).large() // bigger detail panel (small() shrinks it)dio(token).hide() // shipped, but masked until you click to revealsize('sm'|'md'|'lg') is the general form (small()/large() are shorthands; an
unknown value is ignored). hide() marks the list row hidden and puts a
placeholder in the detail panel — good for noisy or sensitive payloads. Same
method names as the PHP and Python clients.
Your own dump types (macros)
Register how a domain object should be dumped once, then send it by name:
const { extend, custom } = require('dumpio-client') extend('invoice', (i) => ({ type: 'invoice', message: `Invoice ${i.number}`, total: i.total }))custom('invoice', order)custom('invoice', order, { flag: 'green', channel: 'billing' })The maker returns the payload to send; without an explicit type the macro's
name is used, and the viewer renders an unknown type with the generic value view.
A name that collides with a real helper is refused. hasMacro(name) and
forgetMacros(name?) round it out. Same registry in the PHP and Python clients.
Timing a request as a tree (spans)
A stopwatch measures one thing. A span measures a section of work and knows what it contains, so the request timeline in the viewer becomes a tree:
const { span } = require('dumpio-client') const s = span('handler', 'controller')const db = span('load orders', 'db')// … queries here appear under "load orders" …await db.end()await span('render', 'view', () => renderPage()) // callback form, sync or asyncawait s.end()The second argument is a free-form category (db, view, http, …) that
colours the bar. Spans opened inside another become its children automatically —
the open stack rides in AsyncLocalStorage, so two requests served at the same
time never nest into each other. Prefer the callback form: it ends the span
even when the work throws or the promise rejects, and the error continues on its
way untouched.
Timing — stopwatch
const sw = stopwatch('import')// … work …sw.lap('parsed') // split time, keeps running// … more work …sw.stop('done') // final timeEach lap()/stop() sends a measure dump with the elapsed milliseconds and the
heap-memory delta since the stopwatch was created.
Typed helpers
Each renders a dedicated view in the viewer, sets schemaVersion: 1, and picks a sensible default flag. The last argument is always opts = { flag?, channel?, timestamp? }.
| Helper | Default flag | Renders |
|---|---|---|
dumpio(v, { label }) |
blue | faithful var tree |
dumpioMessage(msg, extra?, opts?) |
blue | generic titled data |
dumpioException(err, context?, opts?) |
red | error + stack trace |
dumpioQuery(sql, bindings?, timeMs?, opts?) |
purple | SQL query (opts.connection, opts.message) |
dumpioHttp({ method, url, status?, headers?, body?, responseTime? }, opts?) |
by status | HTTP request |
dumpioLog(level, msg, details?, opts?) |
by level | log |
dumpioModel(class, { attributes, relations?, exists?, connection? }, opts?) |
one domain object | |
dumpioCollection(items, opts?) |
list (opts.message) |
|
dumpioTable(columns, rows, opts?) |
explicit table (opts.message) |
|
dumpioMeasure(name, timeMs, { memory?, context? }?, opts?) |
one timing | |
dumpioPerformance({ metrics, breakdown?, context? }, opts?) |
a metric bundle | |
dumpioEvent(event, { entity?, entityId?, actor?, data?, metadata? }?, opts?) |
a domain event | |
dumpioHtml(html, { message?, title? }?) |
an HTML fragment/page in a sandboxed iframe | |
dumpioMail({ html?, text?, subject?, from?, to?, cc?, bcc?, attachments? }, opts?) |
a rendered email (envelope + body) | |
dumpioJson(json, { message?, title? }?) |
a JSON document (string or any value) as tree + source | |
dumpioRequest({ method, url, route, status, time, ip, user }, opts?) |
by status | one inbound request, summarized |
dumpioXml(xml, { message?, title? }?) |
an XML document as an element tree + indented source | |
dumpioImage(url, { message?, alt?, width?, height?, mime? }?) |
an image — data: URI inline, remote URL behind an opt-in |
|
dumpioNotify(message, { title? }?) |
yellow | a desktop notification, raised as soon as it lands |
Flag by HTTP status: undefined → blue, ≥ 500 → red, ≥ 400 → yellow, ≥ 300 → blue, else green.
Flag by log level: emergency/alert/critical/error → red, warning/notice → yellow, info → blue, else gray.
Examples
const { dumpioException, dumpioQuery, dumpioHttp, dumpioLog, dumpioTable, dumpioEvent,} = require('dumpio-client') // An error with a structured stack trace and contexttry { await gateway.charge(order)} catch (err) { dumpioException(err, { orderId: order.id, user: user.email }) throw err} // A database query with parameters and timedumpioQuery('select * from orders where status = ?', ['paid'], 12.4) // An outgoing HTTP call — the flag is derived from the statusdumpioHttp({ method: 'POST', url: 'https://api.stripe.com/v1/charges', status: 402, responseTime: 240 }) // A log line with details (flag by level)dumpioLog('error', 'Payment failed', { gateway: 'stripe', code: 'card_declined' }) // A table — columns + rowsdumpioTable(['id', 'status'], [[1, 'paid'], [2, 'pending']], { message: 'Orders' }) // A business event with an actor and datadumpioEvent('order.refunded', { entity: 'Order', entityId: 42, actor: user.email, data: { amount: 1990 } })For a detailed look at how Dumpio renders each type (and how it parses exceptions), see Dump types and Exceptions.
Request correlation & summaries
Every dump made while serving one request can share a requestId, so the viewer
groups them under one collapsible header — "what happened in this request".
The middlewares do it for you. They open a scope around the whole request and,
when the response finishes, send a request summary (method, url, route,
status, total time, IP) that the viewer puts on the group's header and uses as
its front page:
app.use(expressMiddleware()) // correlation + summaryapp.use(expressMiddleware({ report: 'http' })) // the older inbound-http dumpapp.use(expressMiddleware({ report: false })) // correlation onlyFor code of your own — a worker, a CLI job, a framework we don't ship a middleware for:
const { runInRequest, beginRequest, endRequest, dumpioRequest } = require('dumpio-client') await runInRequest({ label: 'import users' }, async () => { // every dump in here shares one requestId}) // straight-line code can use the simpler pair insteadbeginRequest('nightly rebuild')// …endRequest()Use runInRequest for anything concurrent. It rides on AsyncLocalStorage, so
the scope follows the async call chain — two requests being served at the same
time can never stamp each other's dumps, which a module-level "current request"
could not guarantee. beginRequest/endRequest are a module-level fallback for
straight-line code only.
Middleware for web frameworks
The middleware times each request and, when the response finishes, sends an http dump. It is defensive — it can never crash the host app. Import it from dumpio-client/middleware.
const express = require('express')const { expressMiddleware, installGlobalErrorHandlers } = require('dumpio-client/middleware') const app = express()app.use(expressMiddleware({ channel: 'http' })) // Uncaught errors / rejected promises as `exception` dumps.installGlobalErrorHandlers({ channel: 'errors' })Middleware options: { channel?, headers?, skip? } — headers includes request headers (off by default), skip(req) => boolean filters out requests (e.g. health-check pings).
Koa and Fastify have their own integrations with the same options:
const { koaMiddleware, fastifyPlugin } = require('dumpio-client/middleware') app.use(koaMiddleware()) // Koafastify.register(fastifyPlugin) // FastifyinstallGlobalErrorHandlers() returns an unregister function and does not swallow errors — your existing handlers and the process's default behavior still run.
Auto-capture outbound HTTP
captureHttp() patches node:http / node:https so every outgoing request your app makes — including axios, got and node-fetch, which all funnel through them — is forwarded to the viewer as an http dump. No per-call code needed.
const { captureHttp } = require('dumpio-client') const stop = captureHttp({ channel: 'outbound' })// ... your app makes HTTP calls ...stop() // restore the originals when you're doneIt's idempotent, defensive (a capture failure never disturbs the request), and skips requests to the Dumpio viewer itself, so the SDK never dumps its own traffic. Pass ignore: (info) => boolean to skip noisy endpoints — info carries { method, url, host, port }.
Node's global
fetch(undici) uses its own socket stack, notnode:http, so it isn't captured by this patch on current Node versions.
Auto-capture ORM queries
Attach to your ORM / query builder and every executed statement is forwarded as a query dump (SQL + bindings + timing). Each helper uses the library's own public event API — no monkey-patching — and returns an uninstaller.
const { captureKnex, capturePrisma, captureSequelize } = require('dumpio-client') captureKnex(knex) // Knex — via query / query-response / query-error events // Prisma — the client must be created with query-event logging:const prisma = new PrismaClient({ log: [{ emit: 'event', level: 'query' }] })capturePrisma(prisma) captureSequelize(sequelize) // Sequelize — wraps the `logging` option (keeps yours)Each accepts { channel } (default query). capturePrisma can't detach a Prisma listener, so its uninstaller just stops emitting; the others fully detach.
Forward your logger (console / Pino / Winston)
Send your app's log records to the viewer as log dumps (level → flag), each via the logger's own extension point:
const { captureConsole, pinoStream, winstonTransport } = require('dumpio-client') captureConsole() // console.log/info/warn/error/debug → viewer (still logs) const logger = pino(pinoStream()) // Pino: a destination stream winston.add(winstonTransport()) // Winston: a transport (needs winston-transport)captureConsole() is idempotent and returns an uninstaller; pinoStream() parses Pino's NDJSON (numeric levels mapped, pid/hostname/time dropped from the detail); winstonTransport() forwards { level, message, ...meta }. All take { channel } (default logs).
dumpio-tail — forward log files
For logs that don't go through the SDK (laravel.log, nginx, php-fpm, …), the package ships a standalone agent that tails a file and forwards each new line to the viewer as a log dump. It handles log rotation, detects the level per line, redacts obvious secrets (passwords / tokens / Bearer …), and rate-limits.
npx dumpio-tail storage/logs/laravel.lognpx dumpio-tail --channel nginx --rate 100 /var/log/nginx/error.logOptions: --host / --port / --token (or DUMPIO_* env vars), --channel (default logs), --level (fallback level), --rate (lines/sec, 0 = unlimited), --from-start, --redact <regex> (repeatable), --no-redact. Pairs with the viewer's log mode.
What gets sent (the wire format)
Every dump carries a standard envelope (timestamp, flag, channel, schemaVersion, type, …) plus type-specific fields. The exact envelope, flags, and limits are described in Dump format; the value tree is the same as in the other SDKs.