spekvet
v2026.10.8
Published
A runtime validation and assertion library for server-side invariants.
Readme
spekvet
spekvet is a JavaScript library for runtime validation and invariant checking. Define reusable specs from types, shapes, and predicates, then check the contracts your functions rely on.
npm install spekvetRequire a valid booking
A function relies on assumptions about its inputs. Checking those assumptions at its boundary stops invalid state before it produces misleading results or fails farther from the cause.
import { spec } from 'spekvet';
const booking = spec({
name: spec.string.min(1),
seats: spec.number.and(Number.isSafeInteger).min(1).max(100),
});
function reserve(input) {
const { name, seats } = booking.assert(input, 'Invalid booking');
return `${name}: ${seats} seats`;
}
reserve({ name: 'Ada', seats: 2 });'Ada: 2 seats'spec({ ... }) describes the required fields. Here, name must be a nonempty string and seats a safe integer from 1 to 100. .and() adds a predicate after the number check. Object specs allow extra properties, so a function can check the fields it needs in a richer record.
.assert() returns the original input on success, making it useful in destructuring, assignments, and return statements. If the contract fails, it throws an Error with the mismatch in cause and the complete input in input:
reserve({ name: 'Ada', seats: 0 });Error: Invalid bookingUse .diff() to inspect a failure instead: it returns undefined on success or a mismatch on failure.
booking.diff({ name: 'Ada', seats: 0 });{ kind: 'min', expected: 1, input: 0, path: ['seats'] }The mismatch identifies the failed check, its requirement, and the failing value. Its path locates that value inside the original input. The optional second arguments have different meanings: .assert(input, message) sets the error message; .diff(input, tag) supplies a fallback diagnostic tag.
Check a monitoring station
import { spec } from 'spekvet';
const reading = spec({
unit: spec.any('C', 'F'),
value: spec.number,
});
const latitude = spec.number.min(-90).max(90);
const longitude = spec.number.min(-180).max(180);
const station = spec({
location: [latitude, longitude],
readings: [reading],
attachments: [],
note: spec.optional(spec.string),
});
station.diff({
location: [51.5, -0.12],
readings: [{ unit: 'C', value: 18 }],
attachments: ['photo.jpg', { source: 'manual' }],
});undefinedArray schemas have three forms:
[]accepts any array.[reading]checks every element against a spec.[latitude, longitude]describes an exact-length tuple with a check for each position.
Lists may be empty; use spec([reading]).min(1) to require at least one reading.
spec.any('C', 'F') accepts either literal value.
spec.optional() allows omission or undefined, so the station needs no note.
station.diff({
location: [51.5, -0.12],
readings: [{ unit: 'C', value: '18' }],
attachments: [],
});{ kind: 'type', expected: 'finite number', input: '18', path: ['readings', 0, 'value'] }Specs only check values. Parsing and transformation belong in your domain models or explicit caller functions, where input assumptions and conversion order remain visible. Spekvet does not adopt “parse, don't validate” as its API: it checks contracts without silently repairing values that violate them.
Checks stop at the first mismatch; a failed union contains the mismatch from each alternative. Predicates must return synchronously; async predicates and Promise-returning functions are unsupported. Promises are not awaited and are truthy, so they would incorrectly pass validation. Exceptions from synchronous predicates propagate unchanged. Chaining returns new specs, leaving the originals unchanged.
More ways to compose checks
Require an exact measurement or bound an object’s key count:
import { spec } from 'spekvet';
spec.string.size(2).diff('Ada'); // { kind: 'size', expected: 2, input: 'Ada' }
spec.object.max(1).diff({ a: 1, b: 2 }); // { kind: 'max', expected: 1, input: { a: 1, b: 2 } }Require a defined value, allow null, or match a value by identity:
spec.defined.diff(null); // undefined (only undefined is rejected)
spec.nullable(spec.string).diff(null); // undefined
const token = {};
spec.literal(token).diff(token); // undefinedLabel a failure with application metadata:
spec.string.min(1).tag('name-required').diff('');{ kind: 'min', expected: 1, input: '', tag: 'name-required' }Check relationships between fields, then assert a result before returning it:
const interval = spec({ start: spec.number, end: spec.number })
.and(({ start, end }) => start <= end);
const duration = spec.number.min(0);
function elapsed(input) {
const { start, end } = interval.assert(input, 'Invalid interval');
return duration.assert(end - start, 'Invalid duration');
}
elapsed({ start: 3, end: 8 }); // 5Defer a schema to allow recursive references:
const tree = spec.lazy(() => ({
value: spec.string,
children: [tree],
}));
tree.diff({ value: 'root', children: [{ value: 'leaf', children: [] }] }); // undefinedNext steps
- Tour — Learn the full API, composition, and diagnostics.
- Recipes — Reuse domain rules, check field relationships, and transform validated values.
- Security — Handle sensitive values and bound validation work.
Inspired by Clojure Spec and TigerStyle.
