Most schema libraries describe what shape your data has. databehave
describes how it behaves — distributions, identity, derived values,
invariants, and domain rules — so a single declaration is your TypeScript
type, your runtime validator, and your deterministic sample-data generator.
Scope is deliberately narrow: data schemas, nothing else. No HTTP server, no CLI, no framework adapter — drop it into the stack you already use.
npm install databehave
# or: pnpm add databehave
# or: yarn add databehaveRequires Node.js ≥ 18.17. No transitive dependencies.
v0.3.0early but ready for use within its scope. Breaking changes will follow semver and be noted in CHANGELOG.md. Maintained by @ochairo. Issues and PRs welcome.
One minimal snippet per modelling axis. They compose freely — nest them
inside obj({...}) to build real schemas.
1. Distribution. Shape where values concentrate inside the bounds.
const Price = decimal(10, 2).min('0').max('1000').typically(100, 300)2. Domain. Closed candidate set, optionally keyed off a sibling field.
const Region = str().in(['us', 'eu', 'jp'])
const Currency = str().in({
kind: 'lookup', fromField: 'region',
map: { us: ['USD'], eu: ['EUR'], jp: ['JPY'] },
})3. Derived. Computed from sibling fields, never sampled — stays consistent by construction.
const Total = int().derivedFrom(
ctx => (ctx.parent.qty as number) * (ctx.parent.price as number),
)4. Invariants. Single- and multi-field predicates enforced by rejection sampling.
const Even = int().min(0).max(100).invariant(v => (v as number) % 2 === 0)
const Box = obj({ w: int(), h: int() }).correlate(o => o.w >= o.h)5. Occasional & periodic overrides. Rare random or deterministic exceptions stacked over the base distribution.
const Quantity = int().min(0).max(10).occasionally(-1, 0.01) // 1% chance of -1
const Heartbeat = int().min(0).max(99).eventually(10, 999) // every 10th value is 9996. Discriminated unions. Shape switches on a literal field.
const Variant = discriminated('kind', {
digital: obj({ kind: literal('digital'), downloadUrl: str() }),
physical: obj({ kind: literal('physical'), weightKg: decimal(6, 2) }),
})7. Identity & relations. Stable records keyed by identity, plus cross-dataset foreign keys.
const products = mockDataset({
name: 'products', schema: Product, identity: ['sku'], n: 100,
})
const orders = mockDataset({
name: 'orders', schema: Order, identity: ['id'], n: 50,
fields: { productSku: relate(products, 'sku') },
})8. Weighted choice. Non-uniform discrete sampling.
const Tier = enum_(['A', 'B', 'C']).weighted([['A', 0.7], ['B', 0.2], ['C', 0.1]])Putting it together. Same seed → same value, every run, every platform.
mock(Tier, { seed: 'demo' }) // → 'A' (deterministic)
parse(Tier, 'A') // throws on shape mismatch
type Tier = Infer<typeof Tier> // 'A' | 'B' | 'C'Purity matters. Determinism depends on your code being pure. Any
Math.random()orDate.now()inside aderivedFromcallback silently breaks it. UseexpectStablein CI to catch divergence..normal(...)usesMath.log/Math.cos, which are implementation-defined in ECMAScript and not guaranteed bit-exact across V8 builds; the uniform and.typically(...)paths are bit-exact.
optional/nullable/defaultdo not inject randomness by default. Undermock(), fields with those modifiers always sample a real value. SetMockOptions.modifierProbs(e.g.{ optional: 0.1, nullable: 0.1, default: 0.2 }) to exercise those code paths probabilistically.
databehave ships no server. Write the response body as a pure function of
the request, then bind it to any transport — Hono, Express, Fastify, msw,
node:http.
// items.mock.ts — the only file that touches databehave
import { obj, int, decimal, mockDataset } from 'databehave'
const Item = obj({ id: int().min(1), price: decimal(10, 2).min('0').max('10000') })
export const itemsResponse = (query: Record<string, string>) => ({
result: mockDataset({
name: 'items', schema: Item, identity: ['id'], n: 10,
seedPrefix: `GET:/items:${new URLSearchParams(query).toString()}`,
}),
})Bind it to any transport:
// Hono
app.get('/items', (c) => c.json(itemsResponse(c.req.query())))
// Express
app.get('/items', (req, res) => res.json(itemsResponse(req.query as any)))
// msw (tests)
http.get('/items', ({ request }) =>
HttpResponse.json(itemsResponse(Object.fromEntries(new URL(request.url).searchParams))))See docs/recipes.md for Fastify, node:http, and
stable-seed patterns.
Want a config-driven mock server that already does this wiring? See the companion package
databehave-kit.
- Design — architecture, axis priority, the determinism model.
- Axes — full reference for every axis and modifier.
- Recipes — practical patterns: FK, cadence, lookup domains, snapshot tests.
- Extending — plugin author guide.
- CHANGELOG — release notes.
- CONTRIBUTING — local development workflow.
- SECURITY — how to report a vulnerability.
MIT © 2026-present ochairo. See LICENSE.