How to mock API responses without touching the backend
Stop seeding databases and waiting for backend tickets. Use Map Local to swap any API response with a JSON file on disk — perfect for empty states, error paths, and demos.
Every front-end developer has hit the same wall. You need to build the empty state, the error banner, the new feature flow — and the API to make any of it happen is a sprint away, owned by another team, or sitting in a Jira ticket nobody has picked up. So you stall. You wait. You Slack someone. There is a much faster path, and it lives entirely on your laptop.
This guide is about how to mock API responses for a Mac dev workflow without spinning up a second server, without monkey-patching fetch, and without asking anyone for a database seed. We will use Traceptor’s Map Local feature to point any URL at a JSON file on disk, then layer on a few scripted rewrites for the cases where a static file isn’t enough. By the end you’ll have a tiny library of fixtures that let you build every screen of your app against responses you fully control.
What “mocking” actually means
Half the confusion around mocking comes from the word itself. It gets used for at least three different things, and the workflow you pick depends on which one you actually need.
- Stubbing the client. You patch
fetchor your HTTP client inside the app so it returns a hard-coded object. Easy to start, but it leaks fake data into your production bundle and doesn’t cover anything the SDK or third-party libraries call. - Running a mock server. You boot a separate process (json-server, MSW node, WireMock) on another port and point the app at it. Real network, but now you have two services to keep in sync, and the second one has to be rebuilt every time the contract drifts.
- Intercepting at the wire.A proxy sits between your app and the network and returns whatever you want for matching requests. The app doesn’t know. Nothing in the codebase changes. This is what Traceptor does.
We’re going to focus on the third one, because it’s the only approach that lets you mock API responses for any process on your Mac— your Next.js dev server, your iOS simulator, a Swift CLI, a Postman call, a webhook from Stripe’s CLI — without changing a line of code in any of them.
The Map Local approach
Map Local is the feature that pays for the app. The fastest entry point: right-click any request in Traceptor’s timeline and choose Mock Response— this opens the rule sheet pre-filled with the captured response’s URL pattern, content type, and decompressed body. Save it as a file, hit Add Rule, and you’re done. From that moment on, every request that matches the pattern returns the contents of that file with the response head you configured. The full list lives at Tools ▸ Map Local, where you can flip rules on and off, group them into folders, and edit the response head (status line plus headers). The app under test never knows the difference.
Files default to ~/Library/Application Support/com.traceptor.app/MapLocal/<name>.<ext>, though you can point any rule at any file. Editing the file on disk is hot — the next matching request reads the fresh bytes, no proxy restart needed.
The 30-second loop
Seven patterns that pay for the app in an afternoon
Here are the moves we reach for ourselves, in roughly the order we discovered them. Each one is a different way to mock API responses for a problem you’d otherwise solve with code or a meeting.
1. Mock an unwritten endpoint
The backend ticket for /v2/recommendationsisn’t merged yet, but the design is shipping Friday. Create a file, point Map Local at it, and build the UI against your imagined contract. When the real endpoint lands you flip the rule off — if your fixture matches reality, nothing breaks.
json// ~/fixtures/recommendations.json
{
"items": [
{ "id": "r_01", "title": "Italian leather wallet", "score": 0.92 },
{ "id": "r_02", "title": "Linen summer shirt", "score": 0.88 },
{ "id": "r_03", "title": "Walnut desk lamp", "score": 0.81 }
],
"model": "reco-v2-2026-05",
"generated_at": "2026-06-03T08:14:22Z"
}2. Build a UI without the backend (feature-flag style)
Your designer just handed you a new screen. The endpoint that powers it is two weeks away. You don’t want to wait. Snap a copy of the feature-flag config, flip the new flag on, map local. The whole app now believes the feature is live for your account — on your machine, in your build, only.
json// ~/fixtures/flags-new-onboarding.json
{
"flags": {
"new_onboarding_v3": true,
"show_dashboard_v2": true,
"use_react_router_7": true
},
"experiments": []
}3. Test empty states
Empty states are where products feel cheap. The fastest way to actually design them is to see them with real layout, real typography, real spacing — not a Storybook stub. Map local to { "items": [] }and you’re looking at the real thing in ten seconds.
json// ~/fixtures/inbox-empty.json
{
"items": [],
"next_cursor": null,
"total": 0
}4. Simulate the failure mode QA reported
QA filed a bug that says “the upload screen freezes when the server returns a 500 with no body.” You can’t reproduce it because the server never does that on your machine. Map local to a file, set the status override to 500, watch the freeze. Then fix it.
json// ~/fixtures/upload-500.json
{
"error": "internal",
"request_id": "req_8f1c2e",
"message": "Storage backend returned an unexpected response."
}Status code overrides live on the rule
5. Pin a specific server version
If you depend on a remote /config or feature-flags endpoint that changes underneath you, snapshot it. Capture the response once on a known-good day, save it, map local. Your dev environment stops flickering between configurations and your screenshots stop drifting.
json// ~/fixtures/config-2026-06-03.json
{
"version": "2026.06.03",
"flags": {
"new_onboarding": true,
"billing_v2": true,
"ai_search": false
},
"limits": { "max_uploads_per_day": 250 }
}6. Short-circuit a slow third-party API
Your tests hit Stripe’s sandbox. The sandbox is fine, but it adds 400ms to every run and occasionally rate-limits CI. In dev, point api.stripe.com/v1/customers/*at a local file and watch your test suite drop from 38 seconds to 4. Same trick works for Algolia, Segment, Mixpanel — anything you don’t need a live response from while building UI.
json// ~/fixtures/stripe-customer.json
{
"id": "cus_TestFakeOnly",
"object": "customer",
"email": "dev@traceptor.app",
"subscriptions": { "data": [{ "id": "sub_123", "status": "active" }] }
}7. Demo a feature with no backend dependency
You’re showing the new dashboard to the CEO in twenty minutes and staging is down. Open Traceptor, enable the rule group called demo-dashboard, which already maps half a dozen endpoints to clean, narratively-correct fixtures. Demo proceeds. No one ever knows the database wasn’t talking to you.
When Map Local isn’t enough — scriptable rewrites
Static files cover ninety percent of mocking work. The other ten percent is the case where you need the response to depend on the request: echo a query parameter, flip a feature flag for a specific user ID, inject a header into every call. For that, open Tools ▸ Scriptingand write a small JavaScript snippet. The contract is two async hooks — onRequest(context) and onResponse(context)— both of which receive and return a contextobject. The script runs in a fresh JS context per flow, after Map Remote, Match & Replace, and Header Injection.
js// Echo a query parameter into the response body so the UI gets
// deterministic data for a search/filter while the backend lags.
async function onResponse(context) {
const { request, response } = context;
if (!request.url.includes("/search")) return context;
const url = new URL(request.url);
const query = url.searchParams.get("q") ?? "";
const body = JSON.parse(response.body);
body.query = query;
body.items = body.items.filter((i) =>
i.title.toLowerCase().includes(query.toLowerCase())
);
response.body = JSON.stringify(body);
return context;
}
// Tag every outbound request from this build so it's easy to filter
// out in dashboards once the real backend comes back online.
async function onRequest(context) {
context.request.headers["X-Traceptor-Mock"] = "rewrite-1";
return context;
}Rewrites compose with Map Local — the local file fires first, the script runs against whatever response comes back. So you can keep the bulk of a mock in a JSON file and use the script only to swap the two fields that need to vary per request. That separation keeps your fixtures readable.
Power tips
- Wildcards in URL patterns. Match
https://api.example.com/v1/users/*to mock every user regardless of ID, or*://*/feature-flagsto catch the same endpoint across staging, prod, and your localhost. - Status code overrides. Every rule has its own status, so the same JSON body can be served as a 200 in one rule and a 503 in another. Group both, toggle between them.
- Header injection. Add
Cache-Control: no-storeto a mocked response to defeat your service worker, or stamp anX-Mock-Sourceheader so the devtools network panel makes it obvious which calls are fake. - Rule groups. Save a set of mocks as a group named
demo-dashboard,empty-states, orchaos-mode. One click enables or disables the whole group. - Per-host enable/disable. Mock
api.example.combut letauth.example.comhit the real server, so you keep a valid session while you fake everything else. - Hit counts.Each rule shows how many requests it matched in the current session. Helpful when you’re wondering why your mock isn’t firing — almost always it’s a path mismatch, and the counter says 0.
Why this beats a mock server
Once you’ve done this for a week, going back to running a separate mock server feels archaic. There is no second process to start, no port to remember, no Docker container to rebuild when the schema changes. The mocks live next to the real network, which means you get real CORS, real cookies, real TLS, and the exact build of your app running on the exact machine you’re on. Flip a rule off and the next request is real — no restart required.
That last property is the killer. A mock server forces you to choose, for the whole session, whether you want fakes or reality. Traceptor lets you choose per endpoint, per host, per minute. Build the empty state with fakes, log in for real, place a real Stripe charge, intercept the webhook on the way back, edit it, replay. The whole loop happens on your laptop, with no one’s permission, in the time it takes to refresh a tab.
A small warning
That’s the whole pattern. Capture a request, save its body, edit the file, map local. Repeat until your app has a fixture for every screen that used to depend on the backend, and your unblock time goes from days to seconds.
Keep reading
