Transports
Every transport hands back the same RiftEngine — engine.create(), .get(), .list(),
.intercept(), the full DSL surface. They differ in how the engine is reached, what’s required to
use them, and what they cost.
rift.embedded() |
rift.spawn() |
rift.connect(url) |
|
|---|---|---|---|
| What it does | Loads librift_ffi in-process via FFI (koffi) |
Launches the rift engine binary as a child process |
Attaches to an admin endpoint that’s already running |
| Requires | @rift-vs/rift-embedded installed |
A resolvable rift binary (PATH, cache, or download) |
A reachable Rift server; --api-key if it enforces one |
| Ports | OS-assigned, no network hop | OS-assigned, loopback HTTP | Whatever the remote server is listening on |
| Platform support | Linux/macOS stable; Windows experimental | Linux/macOS/Windows stable | Linux/macOS/Windows stable |
| Best for | Unit/integration tests, zero-Docker CI | Tests/CI that want a real process boundary | CI service containers, a shared dev/staging instance |
| Disposal | await using closes the in-process engine |
await using closes the client and kills the child process |
await using closes the client only (server keeps running) |
rift.embedded() — in-process, zero-config
No Docker, no child process, OS-assigned ports. Requires the companion embedded package:
npm i -D @rift-vs/rift-embedded
If it isn’t installed, rift.embedded() throws EngineUnavailable with an install hint (the
package is a dynamic, optional import — rift.connect()/rift.spawn() never load it or koffi):
embedded transport requires the optional @rift-vs/rift-embedded package — install it (e.g. npm i -D @rift-vs/rift-embedded) to use rift.embedded()
import { rift, imposter, onGet, okJson } from '@rift-vs/rift';
await using engine = await rift.embedded();
const users = await engine.create(
imposter('users').stub(onGet('/api/users/1').willReturn(okJson({ id: 1, name: 'Alice' }))));
await fetch(`${users.url}/api/users/1`);
Choose this when you want the least ceremony: no binary to resolve, no port bookkeeping, and
(Linux/macOS) the fastest startup of the three. Running it as a standalone mock server rather than
inside a test runner? Pass { keepAlive: true } so the process doesn’t exit while the engine is
open — see the rift-core package README
for that variant.
rift.spawn() — managed binary
Launches the rift engine binary as a child process and talks to it over loopback HTTP. Requires a
resolvable rift binary — see Engine binary resolution for the full
lookup order (PATH → local cache → checksummed download, never when air-gapped). Run
npx rift-fetch ahead of time to warm the cache (e.g. in CI) or prepare an air-gapped install.
import { rift, imposter, onGet, okJson } from '@rift-vs/rift';
await using engine = await rift.spawn(); // resolves/downloads the rift binary on first use
const users = await engine.create(
imposter('users').stub(onGet('/api/users/1').willReturn(okJson({ id: 1, name: 'Alice' }))));
await fetch(`${users.url}/api/users/1`);
Choose this when you want a real process boundary (matches how Rift/Mountebank actually run in
production) without managing that process yourself. The trade-off versus embedded() is
resolution/startup cost on first use (or a cold cache) and one more OS process to reason about.
rift.connect(url) — attach to a running engine
For an engine already running elsewhere: a CI service container, a shared dev instance. Nothing is
spawned or loaded — connect() only builds an HTTP client against url. apiKey is sent as
Authorization: Bearer <apiKey> when the server enforces one (started with --api-key).
A blank
apiKeyis rejected. Passing an empty or whitespace-only string throwsInvalidDefinition; omit the option entirely to run without admin auth. This matters most for theprocess.env.RIFT_API_KEYpattern below: an unset variable isundefinedand is fine, but a variable set to the empty string is not. Engine v0.17.0 and later refuse to start with a blank--api-key(rift#862); on earlier engines a blank key switched the auth gate on and then authenticated every request, so the SDK rejects it on both. A key that merely contains spaces is valid and is never trimmed.
import { rift, imposter, onGet, okJson } from '@rift-vs/rift';
await using engine = await rift.connect(url, { apiKey: process.env.RIFT_API_KEY });
const users = await engine.create(
imposter('users').stub(onGet('/api/users/1').willReturn(okJson({ id: 1, name: 'Alice' }))));
await fetch(`${users.url}/api/users/1`);
Choose this when the engine’s lifecycle is someone else’s problem — a shared instance your tests
shouldn’t be creating/tearing down themselves. Because the engine is shared, per-test imposter
isolation doesn’t come for free here the way it does with embedded()/spawn(); see the
rift-core package README’s
“Isolation” section (the spaces pattern) if multiple tests share one imposter.
connect() also compares the connected engine’s reported version against this SDK’s
minEngineVersion by default (versionCheck: 'fail') — pass 'warn' or 'off' to relax that.
Disposal: await using
Every example above opens the engine with await using — explicit resource management
(Symbol.asyncDispose), stable in Node ≥ 20. It calls engine.close() at the end of the
enclosing scope, no try/finally required: for spawn() this also kills the child process, for
embedded() it unloads the in-process engine, and for connect() it only closes the local client
— the remote server keeps running. Without await using you’re responsible for calling
engine.close() yourself (e.g. in a finally block or test-framework teardown hook).