967 words
5 minutes
Stop Mocking Everything: Why I Built a Selective Local Proxy Instead

When you’re building a frontend that depends heavily on backend state, the hardest part isn’t styling components — it’s reproducing the right data at the right time.

This is the story of why I stopped relying on test accounts, avoided fully mocking my API, and instead built a tiny local proxy that gives me complete control without breaking integration.


The Problem#

I was working on a frontend where a single screen could render five completely different UI states depending on what the backend returned:

  • A prepaid user with no bills
  • A prepaid user with unpaid bills
  • A postpaid user
  • A zero-balance edge case
  • An error state

To build and properly test all of these, I needed the backend to return very specific data combinations.

And that’s where everything started falling apart.


Why “Just Use a Test Account” Doesn’t Work#

The obvious solution is to use a test account and hit the real API.

In theory, that sounds fine. In practice:

1. You can’t get accounts for every state#

I needed a prepaid user with exactly zero balance and two unpaid bills. That account didn’t exist — and no one was going to create it just so I could tweak a layout.

2. Accounts don’t stay stable#

Even when I found a “perfect” test account, something would break it:

  • Someone else logs in and changes data
  • A background job runs
  • A scheduled process updates the state
  • Tokens expire

The carefully prepared state from Monday is gone by Tuesday.

3. It’s painfully slow#

Switching accounts means:

  • Logging out
  • Logging in
  • Clearing tokens
  • Waiting for propagation

Do that ten times in a day and your afternoon is gone.


Why Mocking the Entire API Isn’t the Answer#

The next idea is to mock everything — intercept all requests and return fake JSON.

That solves the state problem. But it creates new ones.

1. Schema drift becomes inevitable#

Once you stop calling the real backend, you stop validating your integration.

The backend team:

  • Renames a field
  • Adds a required property
  • Changes a status code

You don’t notice until integration testing — or worse, production.

2. You’re maintaining two backends#

Every mock becomes a contract you now have to maintain.

You’re effectively running:

  • The real backend
  • Your fake backend

And they will drift apart.


Why Mocking Inside Your Code Is Even Worse#

Another approach is mocking at the code level:

  • Feature flags
  • if (process.env.MOCK) conditions
  • Fake services injected into DI
  • Temporary hardcoded responses

It’s the fastest way to get something on screen.

It’s also the fastest way to ship fake data to production.

The problems:#

  • You forget to remove it. You promise yourself you’ll clean it up. You won’t — or you’ll miss one.

  • Mocks get scattered everywhere. Five files. Three services. One forgotten condition.

  • Your production code becomes polluted. Your UI should not know about your development workflow.

Mocking strategy should never leak into business logic.


What I Actually Wanted#

I didn’t want full control.

I wanted targeted control.

Specifically:

  1. Mock only what I’m working on. If I’m building the balance component, I want to control /v1/lines/balance. Everything else should hit the real backend.

  2. Switch scenarios instantly. Move from “prepaid with bills” to “error state” by changing one value. No code edits. No redeploy. No account switching.

  3. Keep mocks outside the app. The frontend should not know it’s being mocked. No feature flags. No fake services. No cleanup later.


The Solution: A Local Proxy with Selective Mocking#

I built a small TypeScript + Express server that sits between my frontend and the real API.

It does two things — in this order:

  1. Check for a mock rule. It reads a scenarios.json file and looks for a matching method + path. If a rule exists and is enabled, it returns the configured response.

  2. Forward everything else. If no mock matches, the request is transparently forwarded to the real backend.

Frontend → Local Proxy (localhost:5050) → Real API Gateway
├─ /v1/lines/balance → mock
├─ /v1/lines → real backend
├─ /v1/information → real backend
└─ /v1/eligibility → real backend

The Configuration#

The behavior is controlled entirely by a JSON file:

{
"rules": [
{
"method": "POST",
"match": "/v1/lines/balance",
"enabled": true,
"active_scenario": "prepaid_no_bills",
"scenarios": {
"prepaid_no_bills": {
"status": 200,
"file": "fixtures/prepaid_no_bills.json"
},
"prepaid_with_bills": {
"status": 200,
"file": "fixtures/prepaid_with_bills.json"
},
"error500": {
"status": 500,
"json": { "message": "Internal Server Error" }
},
"slow": {
"status": 200,
"file": "fixtures/prepaid_no_bills.json",
"delay": 3
}
}
}
]
}

Want to test the error state? Change "active_scenario" to "error500".

Want to simulate a slow API? Switch to "slow".

Want to disable mocking entirely? Set "enabled": false.

No code changes. No rebuild. Just refresh.


What This Gave Me#

1. Real backend everywhere else#

If the backend changes an endpoint I’m not mocking, I find out immediately. No silent schema drift.

2. Instant scenario switching#

The proxy re-reads the config on every request. Edit JSON → refresh → new state.

3. Zero footprint in the frontend#

The app has no idea it’s being mocked. There is nothing to remove before shipping.

4. Shareable fixtures#

Fixtures are real response shapes. Other developers and QA can reproduce exact edge cases without “magic accounts.”

5. Delay simulation#

Adding "delay": 3 lets me test loading states and timeout behavior — something surprisingly difficult when your real backend responds in 50ms.


Lessons Learned#

1. Don’t parse request bodies in the proxy#

I initially added express.json() globally and ran into ECONNRESET errors.

The problem? The body parser consumed the request stream before the proxy forwarded it.

Since the mock layer only needs the method and path, parsing the body was unnecessary — and harmful.

Removing it fixed everything.

2. Mock as little as possible#

The power of this setup isn’t that you can mock everything.

It’s that you don’t have to.

Mock the one endpoint you’re actively working on. Let the rest stay real.

You keep control without sacrificing integration confidence.


Why Build This Instead of Using Existing Tools?#

Tools like mitmproxy or Charles already offer powerful interception features.

I built this for three reasons:

  • I wanted something extremely simple
  • I wanted full control
  • I wanted to understand exactly what was happening

The entire project is around 150 lines of TypeScript and took about two hours to build.

It has already saved me many more hours in debugging and UI validation.

There’s no abstraction layer. No hidden complexity. No black box.

Just a tiny proxy that does exactly what I need.


The Code#

If you’re curious, here’s the GitHub repo:

https://github.com/BoumouzounaBrahimVall/local-proxy/tree/main