The typed DSL
An imposter is built from four kinds of pieces, assembled bottom-up: matchers bind to fields to
become predicates, predicates AND together onto a stub, a stub carries a response cycle, and
stubs collect onto an imposter. Every builder is synchronous and pure — build() produces a
plain wire object, and nothing talks to a server until engine.create(...).
This guide covers the building blocks with examples. For the exhaustive Mountebank-JSON-to-DSL mapping (every predicate operator, behavior, fault, proxy option, script kind) see Migrating from Mountebank; for exact types see the API reference.
Imposters
imposter(name?) returns an ImposterBuilder. The name is optional and purely descriptive
(Imposter.name on the wire); an explicit .port(n) is always respected, otherwise the engine
assigns one.
import { imposter } from '@rift-vs/rift';
imposter('users')
.port(4545)
.https({ cert, key, mutualAuth: true }) // sets protocol: 'https' + inline PEM
.record() // recordRequests — required for verify()/recorded() to see anything
.recordMatches() // recordMatches — predicate-match diagnostics
.allowCORS()
.defaultResponse(status(404, { error: 'no matching stub' }));
.record()turns on the request journal that verification reads from — without it,verify()/recorded()throw naming.record()as the fix..recordMatches()is a separate diagnostic flag (which predicates matched/didn’t on each request), independent of.record()..defaultResponse(...)accepts aResponseBuilderor a rawIsResponse; it must be anisresponse (no proxy/inject/fault) —imposter().defaultResponse(...)throwsInvalidDefinitionotherwise.
Stubs and request matching
stub() is the bare, catch-all opener; the method-seeded openers combine an equals(method)
predicate with a path predicate in one call:
import { stub, onGet, onPost, onPut, onDelete, onPatch, onHead, onOptions, onAny, on } from '@rift-vs/rift';
onGet('/api/users/1') // equivalent to on('GET', '/api/users/1')
onPost('/api/users')
onPut('/api/users/1')
onDelete('/api/users/1')
onPatch('/api/users/1')
onHead('/api/users/1')
onOptions('/api/users/1')
onAny('/api/health') // any method, path must match
on('PROPFIND', '/webdav') // explicit method, for verbs with no dedicated opener
stub() // no seeded predicate at all — refine with .withMethod()/.withPath()/...
Path params. A path containing :name segments — onGet('/users/:id') — seeds both the
stub’s route_pattern (for template/script extraction; route_pattern never matches by itself)
and a derived anchored regex predicate on path (^/users/[^/]+$). Pass { params: false } to
treat : as a literal instead. Param names are captured at the type level only
(StubBuilder<{ id: string }>) for editor hints — it has no runtime effect, and a param-typed
builder composes into every consuming position (imposter().stub(), scenario().when(), the
ImposterHandle stub-surgery methods, verify()).
Predicates and matchers
A matcher (equals, contains, …) is a field-agnostic value; it becomes a Predicate only once
bound to a field:
import { req, equals, deepEquals, contains, startsWith, endsWith, matches, exists, notExists, and, or, not } from '@rift-vs/rift';
req.path(equals('/x')) // or just req.path('/x') — a bare value means equals
req.body(deepEquals({ id: 1 }))
req.header('Accept', contains('json'))
req.path(startsWith('/api'))
req.path(endsWith('.json'))
req.path(matches('^/users/\\d+$')) // string or RegExp
req.query('debug', exists())
req.query('debug', notExists())
and(req.method('GET'), req.path('/x'))
or(req.path('/a'), req.path('/b'))
not(req.header('X-Skip', exists()))
req.method/req.path/req.body/req.header(name, ...)/req.query(name, ...) are the field
binders. On a StubBuilder, .withMethod(...)/.withPath(...)/.withBody(...)/
.withHeader(name, ...)/.withQuery(name, ...) do the same thing, ANDed onto the stub in call
order — useful when refining a seeded opener:
onPost('/api/users').withHeader('content-type', contains('json'))
.when(predicate) accepts a raw wire.Predicate as an escape hatch when nothing else fits.
Every matcher takes modifiers, each returning a new (not mutated) Matcher:
contains('json').caseSensitive()
contains('json').keyCaseSensitive() // for keyed fields (headers/query): match the key case-sensitively too
contains('\\s').except('\\s') // strip a pattern from both sides before comparing
equals(v).jsonpath('$.a.b') // select a JSON body subpath before matching
equals(v).xpath('//a', { ns: 'http://example.com' }) // select an XML body subpath
Responses
import { ok, okJson, created, noContent, badRequest, notFound, status } from '@rift-vs/rift';
ok({ hello: 'world' }) // 200, body as given
okJson({ id: 1 }) // 200 + Content-Type: application/json
created({ id: 1 }) // 201
noContent() // 204
badRequest({ error: 'bad' }) // 400
notFound({ error: 'missing' }) // 404
status(503) // any status code, optional body
Headers, binary bodies, and templating are chainable on any response builder:
ok().header('Set-Cookie', ['a', 'b']) // a string[] value emits a multi-value header
ok().binaryBody(uint8ArrayOrBase64String) // base64-encodes a Uint8Array; a string is trusted as already-base64
ok(template).templated() // marks the body for engine-side template rendering
Response cycling. .willReturn(r1, r2, ...) sets the response cycle the engine advances
through on successive matching calls. .respond(...) is an alias. Repeated .willReturn() calls
on the same stub append to the same cycle rather than replacing it:
onPost('/api/users')
.willReturn(created().latency(50))
.willReturn(status(503)) // now cycles: created, then 503, then created, ...
Behaviors
Behaviors chain on any response builder and compose:
ok().latency(500) // fixed delay, ms
ok().latency({ min: 100, max: 500 }) // random range (Rift extension)
ok().repeat(3) // repeat this response 3x before cycling on
ok().decorate('function(req,res){ ... }')
ok().shellTransform('cmd1', 'cmd2') // one string per command; multiple = chained
ok().copy({ from: 'path', into: '${ID}', using: { method: 'regex', selector: '/users/(.+)' } })
ok().lookup({ key: { from: 'path', using: {...} }, fromDataSource: { csv: { path: 'x.csv', keyColumn: 'id' } }, into: '${ROW}' })
ok().behavior({ /* raw _behaviors escape hatch */ })
Execution order in-engine: copy → lookup → decorate → wait. .latency() never emits the
Mountebank wait: { inject: ... } random-delay form — the engine’s WaitBehavior parser only
accepts a fixed number, a {min,max} range, or a JS function-source string; use the {min,max}
form for a random range.
Faults and proxying (brief)
import { fault, Fault } from '@rift-vs/rift';
fault(Fault.CONNECTION_RESET) // bare top-level fault response
ok(body).withFault(Fault.latency(500, { probability: 0.3 })) // probabilistic, composes onto an `is`
import { proxyTo } from '@rift-vs/rift';
proxyTo('http://upstream').proxyOnce() // record once, replay thereafter
.generatePredicates({ matches: { path: true, method: true } })
Both have a much larger surface (all four native TCP fault kinds, proxyAlways/
proxyTransparent, header injection, path rewriting, mTLS to upstream, and how behaviors compose
onto a proxy response) — see Migrating from Mountebank §Faults
and §Proxy for the complete mapping.
Escape hatches
fromJson(json)— parses and validates a raw Mountebank/Rift imposter JSON (single imposter or{ imposters: [...] }) verbatim: no key renaming, no field injection, no dropped unknown keys, and an explicitportis respected exactly.engine.create(fromJson(json))drops straight in — this is how a raw Mountebank imposter round-trips into the typed layer..raw(patch)— every builder (imposter(),stub(), response builders) has a.raw(patch)that shallow-merges a plain object patch at the top level, applied last, for wire fields with no dedicated method yet.wire.*types —import { wire } from '@rift-vs/rift'gives the exact wire grammar (wire.Imposter,wire.Stub,wire.Predicate,wire.StubResponse, …) for hand-building anything the DSL doesn’t cover, or for typing afromJsonresult.