Skip to content

ochairo/databehave-kit

Repository files navigation

databehave-kit

Config-driven mock server, powered by databehave.

npm version Node.js License: MIT

Most mock servers are frameworks you wire up. databehave-kit is a single JSONC file you point at an OpenAPI document — declared routes win, missing paths fall back to the spec, and a header flips any route to a 4xx/5xx without touching code.

Bundles databehave so the same file can declare the route and the deterministic body it returns. The HTTP framework underneath is hidden: handlers see a stable (req) => { json | text | html | raw | empty } signature.

Opt-in extras (all default-off, additive): an admin panel + REST API for sticky error overrides at /databehave, per-request x-mock-* injection headers, file-backed scenarios, and a one-line-per-request access log.

Install

npm install databehave-kit
# or: pnpm add databehave-kit
# or: yarn add databehave-kit

Requires Node.js ≥ 18.17. JSON OpenAPI documents only — convert openapi.yaml to openapi.json in your build step.

Maintained by @ochairo. Breaking changes follow semver and are noted in CHANGELOG.md. Issues and PRs welcome.

A 60-second tour

One minimal snippet per feature. They compose freely inside a single databehave.config.jsonc.

1. Declarative routes. Route keys are JSON, not app.get(...) calls.

{
  "endpoints": {
    "GET /health": { "response": { "status": 200, "json": { "ok": true } } },
    "GET /products/list": "./src/routes/products/list.js",
    "POST /products/bulk-update": "./src/routes/products/bulk-update.js"
  }
}

2. OpenAPI fallback. Paths absent from endpoints are served from your OAS document. Declared paths always win.

{ "openapi": "./openapi.json", "basePath": "/api/v1", "endpoints": { /* overrides only */ } }

3. Mock-mode middleware. Force any route to a status by config — bodies come from the OAS response schema. Opt in to header-driven overrides (x-mock-status) for ad-hoc curl-based testing.

{
  "mockMode": {
    "enabled": true,
    "allowHeaderOverride": true,                 // optional, default false
    "pathOverrides": { "GET /api/v1/products": 500 }
  }
}
# With allowHeaderOverride: true the header forces the status per request.
curl -H 'x-mock-status: 500' http://localhost:8000/api/v1/products

4. Env interpolation in JSONC. ${VAR} and ${VAR:default} work on any string value. Comments and trailing commas are accepted.

{ "server": { "host": "${MOCK_HOST:127.0.0.1}", "port": "${MOCK_PORT:8000}" } }

5. Programmatic API. Same config object, no JSONC file required.

import { createDataBehaveKitServer, defineDataBehaveKit } from 'databehave-kit'

const server = createDataBehaveKitServer(defineDataBehaveKit({
  routes: {
    'GET /api/v1/ping': () => ({ json: { ok: true } }),
    'POST /api/v1/echo': async ({ json }) => ({ json: await json() }),
  },
}))

5b. Handler-overrides for vitest/jest. Pass pre-resolved handlers through loadDataBehaveKitJsonConfig to skip dynamic import() — essential for running JSONC configs whose handlers are .ts source under plain Node. Keys tolerate ./ prefix and file-extension differences.

import { loadDataBehaveKitJsonConfig, createDataBehaveKitServer } from 'databehave-kit'

const modules = import.meta.glob('./src/routes/**/*.ts', { eager: true })
const handlers = Object.fromEntries(
  Object.entries(modules).map(([p, m]) => [p, (m as { default: unknown }).default]),
) as Record<string, never>

const { config } = await loadDataBehaveKitJsonConfig('./databehave.config.jsonc', { handlers })
const server = createDataBehaveKitServer(config)

6. Web-standard I/O. server.fetch(Request) → Promise<Response> so tests run in-process without sockets.

const res = await server.fetch(new Request('http://localhost/api/v1/ping'))
// → 200 { "ok": true }

Putting it together. Start from the CLI:

npx databehave-kit databehave.config.jsonc
# → [databehave-kit] listening on http://127.0.0.1:8000

Optional flags (all opt-in):

# After the server is listening, open the admin UI in the default
# browser. No-op when adminMode is absent or disabled.
npx databehave-kit --open databehave.config.jsonc

The JSONC config's openapi value can point at either a JSON file or a YAML file (.yaml / .yml) — YAML inputs are transcoded to JSON on load, so a separate sync-openapi build step is not required. yaml is an optional peer dependency: if your spec is YAML, also npm install yaml alongside databehave-kit. The kit declares exactly one runtime dependency (databehave) — everything else is opt-in.

Programmatic API

The CLI is a thin wrapper around the same run() entry library consumers can import directly. This is the recommended integration shape — your project owns its tsx (or other TS loader) invocation, and the kit stays a normal library import.

// server.ts
import { run } from 'databehave-kit'

await run({
  config: './databehave.config.jsonc',
  open: true,
})
// package.json — use `tsx watch` for dev auto-restart
{ "scripts": { "start": "tsx watch server.ts" } }

run() resolves AFTER the server has emitted listening, so the --open log line still appears in deterministic order. The returned RunHandle is:

interface RunOptions {
  /** Path to the JSONC config. Resolved relative to `process.cwd()`. */
  config: string
  /** After listening, open the admin URL in the default browser. */
  open?: boolean
}

interface RunHandle {
  /** Bound HTTP URL, e.g. "http://127.0.0.1:8000". */
  url: string
  /** Idempotent. Tears down the listener. */
  close(): Promise<void>
}

The same log lines you see from the CLI (listening on …, --open: …) come through console.info / console.warn — the kit deliberately ships no logger abstraction.

The kit does not ship a file-system watcher. Use tsx watch server.ts (or any process supervisor) in dev — the supervisor respawns the entry file on change, which re-invokes run() from scratch.

Loading TypeScript handlers

The handler files referenced from databehave.config.jsonc may be written in TypeScript. The kit ships zero loader machinery; pick a TS loader (tsx, ts-node --esm, @swc-node/register, …) and launch your entry file through it.

The recommended pattern is the programmatic API above — a one-line server.ts invoked through tsx server.ts. The same tsx invocation transparently resolves both the entry file and the dynamically-imported .ts route handlers.

If you'd rather not introduce an entry file, the binary also works under a TS loader:

node --import tsx node_modules/.bin/databehave-kit ./databehave.config.jsonc

Add tsx to your project's devDependencies (yarn add -D tsx). The kit does not list tsx as a runtime dep on purpose.

Admin mode (error injection)

Default-off. Flipping adminMode.enabled: true mounts an error-injection panel at ${path} (default /databehave) and starts honouring x-mock-* request headers. Existing consumers that do not set adminMode see no behaviour change — admin routes return 404 and x-mock-* headers are ignored.

The UI is shipped as a static dist/admin/{ui.html, ui.js, ui.css} bundle built from native Web Components (no runtime UI framework, no CDN dependencies). It groups operations by OpenAPI tag, lets you inject errors per route with auto-pre-fill when an override already exists, exposes a Try-it-out that hits the real endpoint so the injected override applies, and saves / loads named scenarios to adminMode.scenariosDir. English-only.

A dedicated Global override button in the top bar opens a modal that owns global-scope overrides (the per-endpoint inject panel only offers Exact and Path scopes). The right-side active-overrides side panel flags global rows with a distinct GLOBAL pill and floats them above path / exact rows; a bottom-right floating pill shows the active override count. Keyboard shortcuts: /, Cmd/Ctrl+K, Esc, ?.

{
  "adminMode": { "enabled": true }
}
import { createDataBehaveKitServer } from 'databehave-kit'

const server = createDataBehaveKitServer({
  routes: { 'GET /ping': () => ({ json: { ok: true } }) },
  adminMode: { enabled: true },
})
await server.listen({ port: 8000, host: '127.0.0.1' })
// stdout: [databehave-kit] admin panel ready at http://127.0.0.1:8000/databehave (dev mock — disable in production)

Defaults

Field Default Notes
enabled false Opt-in. No surface change unless flipped.
path /databehave Mount point. Must start with /.
ui true Serve the admin HTML page at ${path}.
headers true Honour x-mock-* request headers.
bind loopback-only Refuse listen() on non-loopback hosts. Set any to opt out.
cors auto * when bind is loopback, same-origin when bind is any.
allowDestroy true Honour x-mock-destroy: 1 socket drops.
openapiBody populated by CLI Raw OAS JSON used by the UI route picker.
scenariosDir ${cwd}/mock-scenarios Directory used by the file-backed scenarios endpoints.

Per-request x-mock-* headers

Header Effect
x-mock-status: 500 {kind:'http-status', status:500}
x-mock-business-failure: <ASCII msg> {kind:'business-failure', message}
x-mock-business-failure-b64: <base64-utf8 msg> unicode-safe variant
x-mock-business-failure-extra: <base64 JSON> merges into extra
x-mock-body: <base64 JSON> {kind:'custom-body', body}
x-mock-body-status: 422 sets the response status
x-mock-body-content-type: text/plain sets the content-type
x-mock-empty: 1 (+ x-mock-empty-status) {kind:'empty-body'}
x-mock-malformed: 1 (+ x-mock-malformed-status) {kind:'malformed-json'}
x-mock-delay: 2000 wraps any other header mode OR delays passthrough
x-mock-hang: 1 never resolve — client timeout / hang detection
x-mock-destroy: 1 drop the socket (gated by allowDestroy)

Base64 is standard (not URL-safe). Non-ASCII must use the -b64 variant — HTTP headers reject byte values > 255. Conflicting terminal headers or invalid base64 / JSON return HTTP 400 with an explicit {error: "invalid …: <reason>"} body — the typo is loud, not silent.

curl -sS -H 'x-mock-status: 500' http://127.0.0.1:8000/api/ping
# → HTTP 500 {"error":true,"status":500,"injected":"http-status"}

Sticky overrides — REST API

Stickies persist in-memory across requests until removed or process restart. Resolution priority: header → sticky exact (METHOD+path) → sticky path → sticky global → passthrough.

Method Path Body Notes
GET ${path} admin UI HTML
GET ${path}/ admin UI HTML (trailing-slash variant)
GET ${path}/ui.js admin UI script bundle
GET ${path}/ui.css admin UI stylesheet
GET ${path}/openapi-routes {routes, discoveredAt} for the UI
GET ${path}/overrides {overrides: [...]}
POST ${path}/overrides {matcher, mode, description?} 201 with {id, override}
DELETE ${path}/overrides/:id 200 or 404
DELETE ${path}/overrides clears all; returns {cleared:N}
GET ${path}/scenarios {scenarios: [{name,count,created}]}
POST ${path}/scenarios {name, overrides[]} 201 — saves to disk
GET ${path}/scenarios/:name {scenario: {...}} or 404
DELETE ${path}/scenarios/:name 200 or 404
POST ${path}/scenarios/:name/load clears + re-adds; {loaded:N} or 404

Matchers:

{ "kind": "exact", "method": "POST", "path": "/api/v1/example" }
{ "kind": "path",  "path": "/api/v1/example" }   // any method
{ "kind": "global" }                              // every route

Scenarios

A scenario is a named snapshot of the current sticky overrides, persisted to disk under adminMode.scenariosDir (default ${cwd}/mock-scenarios). Names are restricted to [A-Za-z0-9_-]{1,64}. Writes are atomic (tmp + rename); a missing directory is treated as "no scenarios" rather than an error.

The REST shape is part of the table above (GET/POST/DELETE …/scenarios, GET/DELETE …/scenarios/:name, POST …/scenarios/:name/load). load clears the active overrides and re-adds the snapshot atomically.

Security model

Admin-mode is a developer tool — not a hardened endpoint. The defaults are defensive but do not expose adminMode to untrusted networks:

  • Startup notice. Every enabled boot logs admin panel ready at http://…/databehave (dev mock — disable in production) to stdout so misconfiguration is visible.
  • Loopback-only by default. bind: 'loopback-only' refuses server.listen({ host: '0.0.0.0' | LAN-IP }) with a clear error. Override with bind: 'any' when binding to LAN is intentional.
  • CORS auto-policy. cors: 'auto' returns Access-Control-Allow-Origin: * on loopback (so a localhost browser bookmarklet works) and switches to same-origin (no CORS headers) when bind: 'any'. Explicit cors: 'any' / 'same-origin' / { origin } override the auto policy.
  • allowDestroy: true. Honours x-mock-destroy socket drops. Set false to reject those requests with 503 instead.

Migration tip

Two-line opt-in for an existing consumer — add the field and rebuild:

{ /* existing config */, "adminMode": { "enabled": true } }

Nothing else changes. Remove the field (or set enabled: false) to return to the pre-admin behaviour exactly.

Access logs

Opt-in per-request log line. Off by default — no stdout output and zero per-request overhead until you enable it.

// shorthand: pretty one-liner, auto colours, admin traffic hidden
{ "log": true }

Or as an object:

Key Type Default Notes
access boolean true One line per request when true.
includeAdmin boolean false Include hits to adminMode.path/*.
colors 'auto' | 'always' | 'never' 'auto' 'auto' honours process.stdout.isTTY.
format 'pretty' | 'json' 'pretty' 'json' emits one newline-terminated object.

Pretty sample (colours stripped):

GET /api/v1/health → 200 4 ms · 56
POST /api/v1/widgets/copy → 500 2 ms · 78 [override:http-status]

JSON sample:

{"t":"2026-05-28T10:00:00.000Z","method":"GET","path":"/api/v1/health","status":200,"ms":4,"bytes":"56"}

Lines go to stdout. The admin enable banner also writes to stdout (console.info); stderr carries only runtime warnings and errors. Sticky / header-driven overrides are flagged with [override:<mode.kind>] (pretty) or "override":"<kind>" (json).

Documentation

  • Design — architecture, dispatch pipeline, why the HTTP framework is hidden.
  • Config — full databehave.config.jsonc reference: endpoints, basePath, env interpolation, CORS, mockMode, adminMode, log.
  • Admin mode — admin UI, REST endpoints, matchers, ErrorMode reference, x-mock-* headers, scenarios, security notes.
  • OpenAPI — OAS fallback walker, empty-schema and walk-error reporting, deterministic seeding.
  • Recipes — error injection, static responses, programmatic hooks, in-process tests with fetch().
  • CHANGELOG — release notes.
  • CONTRIBUTING — local development workflow.
  • SECURITY — how to report a vulnerability.

License

MIT © 2026-present ochairo. See LICENSE.

About

Config-driven mock server, powered by databehave.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages