Record a real API and replay it
The API already exists. Writing mocks for it by hand is the wrong way to spend an afternoon, and anything you write by hand is a guess about what it returns.
apifae record proxies the real thing, captures what actually came back, and
turns it into mocks you can serve offline.
Record
You need a workspace first — record stores its sessions inside one:
apifae init -N
Then point the proxy at the API and send your traffic through it:
$ apifae record http://localhost:9100 -p 4555 -n demo
● Recording → http://localhost:9100 (proxy on :4555)
Session: demo
Intercept: off (recording works; --init-ca adds HTTPS proxy interception)
[11:46:15] GET /orders → 200 (2ms, 576b)
[11:46:16] GET /reports/daily → 500 (0ms, 68b)
2 requests captured | Press Ctrl+C to stop
Send requests to the proxy port rather than to the API, and it forwards them on:
curl http://127.0.0.1:4555/orders
-n names the session. Name it — the default is a timestamp, and you will want
to say which capture you mean when you convert it.
On Ctrl+C it summarises what it has:
● Recording stopped.
Session: demo
Target: http://localhost:9100
Duration: 2s
Captured: 3 requests (2 unique endpoints)
Stored: …/.apifae/recordings/demo
Run apifae record --list to see all sessions.
Nothing is a mock yet. The session is raw capture, one file per request:
.apifae/recordings/demo/
├── 001_GET_orders.yaml
├── 002_GET_orders.yaml
├── 003_GET_reports_daily.yaml
└── metadata.yaml
Convert
$ apifae record --convert -n demo --non-interactive
i Converting session "demo"...
Generated mocks:
+ …/mocks/reports/daily.yaml (1 endpoint, 1 response) [NEW]
+ …/mocks/orders.yaml (1 endpoint, 1 response) [NEW]
Summary: 2 endpoints, 2 responses, 2 created, 0 updated, 0 skipped
EXIT: 0
Three requests became two endpoints, because two of them hit the same path.
--non-interactive makes this scriptable. With more than one session present and
no -n, it refuses and lists what is available rather than guessing — so a CI
step cannot silently convert the wrong capture.
When one endpoint answered more than once
The line above hides a decision. Two of those three requests hit /orders, and
only one response ended up in the mock. By default, conversion keeps one
response per status code and discards the rest. That is usually what you want
— a hundred captures of the same list should not become a hundred mock responses
— but it is worth knowing before you assume a capture is complete.
--responses chooses. Say /orders returned three different 200s: two lists
with different rows, then an error object.
apifae record --convert -n demo --non-interactive --responses full
| keeps | from the example | |
|---|---|---|
singular |
the first response per status code — the default | 1 response |
compacted |
one per unique status and body shape | 2 — the two lists collapse, the error object survives |
full |
every response, in capture order | 3 |
compacted is the useful middle: it drops repeats that differ only in their
values, and keeps the ones that differ in structure — which are the ones your
client code branches on.
Extra responses do not rotate. With several responses and no when:, up
serves the first one to every request:
- path: /orders
method: GET
responses:
- status: 200 # this one, every time
body: [{ id: 1, total: 100 }]
- status: 200 # kept as a record, inert until something selects it
body: { error: rate limited }
They are a record of what the API actually returned, not automatic variation.
To serve one deliberately, give it a when: matcher, or drive the endpoint from
a scenario.
Now stop the real API and serve the capture:
apifae up
The recorded bodies come back, byte for byte.
Recording HTTPS
Most real APIs are HTTPS, and a proxy cannot read that traffic without being
trusted. --init-ca generates a certificate authority for the purpose:
apifae record --init-ca https://api.example.com
Try it without touching your system trust store first. curl will trust a CA
for exactly one invocation, which is enough to prove the setup works and costs
nothing to undo:
curl --proxy http://127.0.0.1:4001 \
--cacert ~/.apifae/ca/apifae-ca.pem \
https://api.example.com/some/path
That is the machine-wide CA, which is where record writes one today. If this
workspace has a .apifae/ca/ directory of its own, that one is used instead —
record prefers a workspace-local CA when it finds one, which is how workspaces
created before the machine-wide store keep working. Run apifae ca list if you
are not sure which you have; it prints the path in use.
Without --cacert the same request fails to connect — that failure is the CA
doing its job, not a bug. Only once that works is it worth installing the CA
system-wide, and only if you need browsers or an SDK to go through the proxy too.
When you are done, take it back out:
apifae ca list
apifae ca remove
ca remove deletes what APIFae wrote. It cannot untrust a certificate you
installed into the system store — that needs your own tooling, and the command
prints the steps rather than pretending it has done them.
Committing a capture as a baseline
A converted capture is deterministic: convert the same session twice and you get byte-identical files, with headers and matchers in a stable order. That is what makes this workflow work:
apifae record --convert -n demo --non-interactive
git add mocks/ && git commit -m "baseline: orders API, 2026-08-27"
Re-record against production later, convert again, and git diff shows exactly
what the API changed — nothing else. A converter whose output re-ordered itself
on every run would bury one real change in fifty spurious ones and the diff would
be unreadable.
For the same question asked continuously rather than at capture time, use
diff, which compares without re-recording.
What a session does not contain
A recorded request keeps its header names but not their values:
request:
method: GET
path: /orders
headers:
accept: application/json
authorization: <redacted>
user-agent: curl/8.4.0
You can still see the request was authenticated; you cannot recover what with. The API itself received the real header — redaction happens on the way to disk, not on the way upstream.
Query values are treated the same way when their name looks like a secret
(api_key, token, signature, password and similar); ?status=active is
kept as it is.
--no-redact turns this off for a run, and record.redact: false for a
workspace. apifae init adds .apifae/ to your .gitignore so a session is
not a candidate for git add . either way.
A note on what you capture
Recorded traffic is real traffic. Credentials no longer reach a mock file — request headers are redacted at capture, and response body values under credential-shaped keys are redacted as the mock is written — but everything else does: personal data, identifiers, and any secret under a key nothing recognised.
Read a capture before you commit it. .apifae/recordings/ holds the exchange,
and the generated mocks carry the response headers and bodies. See
Secrets and redaction for exactly what is
kept and how to change it.
Values in recorded content that contain {{ or {% are wrapped in
{% raw %}…{% endraw %} automatically, so captured braces replay as themselves
rather than being interpreted as templates.