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:
-
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. -
Switch scenarios instantly. Move from “prepaid with bills” to “error state” by changing one value. No code edits. No redeploy. No account switching.
-
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:
-
Check for a mock rule. It reads a
scenarios.jsonfile and looks for a matchingmethod + path. If a rule exists and is enabled, it returns the configured response. -
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 backendThe 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