Skip to content

Record and replay: deterministic API mocks for tests and demos

Use the record and replay pattern to turn real API responses into fixtures: how matching works, how to keep secrets out of recordings, and when to re-record.

The ProxifyEdge team 5 min read

Tests that call real third-party APIs are slow, flaky and occasionally expensive. Hand-written mocks fix that, but they drift: the API changes a field and your mock keeps returning last year’s shape. Record and replay sits between the two. You run your code once against the real API and capture the responses; every run after that replays the captured responses instead of calling the network.

This pattern, often called the VCR pattern after the Ruby library that popularized it, gives you fixtures that are real, deterministic and cheap. This guide covers how it works, how requests are matched to recordings, how to keep credentials out of fixtures, and how to keep recordings fresh.

How record and replay works

A recording layer sits between your code and the upstream API, usually as an HTTP client wrapper or a proxy. It has a mode:

ModeWhat it does
offPasses requests through and records nothing.
recordAlways calls the upstream and stores the response, replacing any earlier recording for the same request.
replayServes only from recordings and never touches the network. A request with no recording fails loudly.
autoReplays a recording if one exists; otherwise calls the upstream and records it.

Recordings are grouped into cassettes: named sets, usually one per test suite or scenario, so a “checkout” suite and a “search” suite do not step on each other.

A typical workflow:

  1. Run the suite in auto (or record) mode against the real API once, on a machine that has network access and valid credentials.
  2. Commit or store the cassette.
  3. Run the suite in replay mode everywhere else, including CI. It is now hermetic, offline and deterministic.
  4. When the API changes, delete the cassette and record again.

The value of replay over auto in CI is strictness. In auto, a test that makes a new, unrecorded request quietly reaches the network and records it, which is exactly the flakiness you were trying to remove. In replay, it fails, telling you a recording is missing.

Matching: which recording answers which request

The core design question is how a recording layer decides that an incoming request is “the same” as one it recorded. Match too strictly and nothing ever hits; match too loosely and the wrong response comes back.

A sensible match key includes:

  • The method. A GET and a POST to the same URL are different requests.
  • The target URL, with query parameters normalized. ?a=1&b=2 and ?b=2&a=1 ask for the same resource, so parameter order should not matter.
  • The request body. Two POST requests with different payloads should not share a response.
  • A small set of headers that change the representation, such as Accept, Content-Type and Accept-Language.

Just as important is what the key leaves out. Headers like User-Agent, request IDs, trace IDs and timestamps change on every run. If they were part of the key, a cassette recorded on one machine would never match a request from another.

If your requests contain values that change every run, such as a timestamp in the body or a random nonce, make them deterministic in test mode, or no recording layer can match them.

Keeping secrets out of recordings

Fixtures get committed, attached to bug reports and shared with colleagues. Anything sensitive in them leaks eventually, so a recording layer must clean responses before storing them:

  • Drop credential-bearing response headers such as Set-Cookie, WWW-Authenticate and Authorization. A recorded session cookie is a live credential sitting in a file.
  • Drop headers that describe one delivery rather than the content: Date, Content-Length, Connection, Transfer-Encoding, Age. Replaying them produces a response that contradicts itself.
  • Redact known secret values wherever they appear. Some APIs echo a token back in a custom header or in the body. A name list cannot catch that, but if the recording layer knows the secret it substituted into the request, it can search for that exact value and replace it with a visible marker.
  • Never record request credentials. The match key should not depend on Authorization either, or rotating a test key invalidates every cassette.

Review a cassette before committing it, especially the first time you record against a new API. Search it for your account email, tokens and anything that looks like personal data.

Recording at the proxy instead of in the client

Library-based recorders hook into an HTTP client in one language. That is fine for backend tests, but it is awkward for:

  • Browser tests with Playwright or Cypress, where the requests come from a real browser.
  • Demos and sales environments, where you want the app to behave exactly as it did when you prepared the demo, even if the upstream is down or rate-limits you.
  • Polyglot systems, where the frontend, a worker and a mobile app all call the same API.

If those requests already go through a proxy, the proxy can do the recording, and every client gets replay without code changes. The page selects a cassette with a header or query parameter; the proxy’s mode decides whether to record or replay.

Record and replay with ProxifyEdge

ProxifyEdge can act as that fixture server for requests sent through it. Each API key has a replay mode, set on the dashboard’s Replay page: off, record, replay or auto, with the meanings above. Requests choose a cassette with the X-Proxify-Cassette header or the cassette query parameter, and use default when they name none:

await fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent('https://api.example.com/catalog')}`, {
  headers: {
    'X-API-Key': 'pk_your_test_key',
    'X-Proxify-Cassette': 'checkout-tests',
  },
});

Requests are matched on the method, the target (with parameters in any order), the body, and the Accept, Content-Type and Accept-Language headers. Replayed responses carry X-Proxify-Replay: HIT; in replay mode a request with no recording gets a 404 with X-Proxify-Replay: MISS. Credential headers are dropped before storage, and values substituted from the secrets vault are redacted wherever they appear in a recording. To re-record, delete the cassette and run again in record or auto. The replay docs list the details.

Use a separate key for tests, set to the mode you want, so production traffic is never served from a fixture.

When not to replay

Record and replay is for reads and for deterministic scenarios. Be careful with:

  • Non-idempotent writes. Replaying “create order” returns the recorded response without creating anything. That is often what you want in a test, but not if a later step reads the order back from the real API.
  • Time-sensitive data. A recorded token that expires, or a recorded “current price”, will mislead any assertion about freshness.
  • Contract testing. Replay proves your code handles the responses you recorded. It does not prove the API still returns them. Re-record on a schedule, or run a small live suite separately, so drift shows up.

Key takeaways

  • Record once against the real API, then replay deterministically everywhere else.
  • Use replay in CI so a missing recording fails instead of silently reaching the network.
  • Match on method, normalized URL, body and representation headers; ignore volatile headers.
  • Strip credential headers and redact known secrets before anything is stored, and review cassettes before committing.
  • Recording at a proxy gives browser tests and demos replay without changing client code. For other testing-adjacent patterns, see batching requests and WebAssembly at the edge.

Are you sure?