How-to

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.

The Traceptor team7 min read

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

Capture a real request once. Save the response body to a file. Edit the file to whatever shape you want. Map Local back to that file. Reload your app. That cycle is the entire workflow — and most teams only learn it once they’ve been blocked on a backend ticket for two days.

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

Every Map Local rule has a status field. Leave it at 200 by default, or set it to 401, 429, 500, 503 — whatever the bug report says. The body comes from the file; the status, headers, and latency come from 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-flags to 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-store to a mocked response to defeat your service worker, or stamp an X-Mock-Source header 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, or chaos-mode. One click enables or disables the whole group.
  • Per-host enable/disable. Mock api.example.com but let auth.example.com hit 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

Mocks are tools, not a substitute for integration tests. Keep the fixtures in version control next to the feature that uses them, and delete them once the real endpoint is stable. Otherwise the team that owns the backend will eventually change a field and you will ship a bug because your local fake disagreed with reality for six weeks.

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