A frontend team rolled out a type-safe API-mocking layer that lives only in development. Using Axios interceptors and Vite’s tree-shaking, the production bundle stays untouched. Engineers fetch data with their usual patterns while waiting for backend endpoints, then flip a single environment flag to hit the real API.

Why the team needed a better way to mock

Frontend developers hit a roadblock when a backend route is unfinished. The quick fix—hard-coding a response inside a component or sprinkling if (process.env.NODE_ENV === 'development') blocks throughout the UI—keeps the app moving but leaves technical debt. Those mock objects become part of the component’s logic, raise the risk of shipping fake data to production, and make the code harder to read and test.

The team wanted to move every mock out of the component tree, enforce a contract between front and back ends, and guarantee that nothing extra ends up in the production build.

The three-step process the team follows

  1. Contract meeting – Frontend and backend engineers sit down and list each request, its URL, method, and expected payload.
  2. Typed contract – They turn the list into a TypeScript interface that becomes the single source of truth for request and response shapes.
  3. Interceptor wiring – An Axios interceptor examines every outgoing request. If the URL matches a registered mock, the interceptor returns the mock data; otherwise the request proceeds to the live server.

Because the interceptor is the only place the mock logic lives, component code stays unchanged. Developers keep using their normal data-fetching hooks—such as useQuery—without adding any conditional logic.

How production bloat is avoided

The team layered three safeguards that let Rollup (the bundler used by Vite) strip the mock code entirely when building for production:

  • import.meta.env.DEV resolves to false in a production build, so the entire interceptor module disappears during tree-shaking.
  • The MODE variable is set to something other than test when running unit tests, keeping test-only code separate.
  • A custom flag, VITE_ENABLE_MSW, defaults to false and must be turned on explicitly for mock activation.

When all three conditions are false, the mock registry never makes it into the final bundle.

Organising the mock files

The repo follows a feature-centric layout:

  • interfaces/ – Holds the TypeScript definitions generated from the contract meeting.
  • scenarios.ts – Contains concrete examples of successful responses and error cases for each endpoint.
  • devHandlers.ts – Acts as the central registry that maps URLs to the scenario data and plugs the interceptor into Axios.

A small scaffolding script can generate these files automatically: give it a URL and the matching interface, and it creates the stub files and registers the mock. The script lives outside the production code path, so it does not affect bundle size.

What the team gained

  • Zero mocks inside components – All fake data lives in a dedicated layer, keeping UI code clean.
  • End-to-end type safety – Mock data conforms to the same TypeScript interfaces used by real responses, so mismatches are caught at compile time.
  • No production weight – Tree-shaking removes the interceptor and mock data entirely, leaving the bundle size unchanged.
  • Shared scenarios for dev and test – The same mock definitions drive both local development and automated tests, reducing duplication.

The trade-offs and limits

The approach does not replace a real backend. If the mock contract diverges from the live API, developers discover the mismatch only after they flip the environment flag.

What to watch next

  • Tooling integration
  • Broader adoption
  • Performance monitoring

The takeaway is clear: moving mock logic into a typed, environment-gated layer lets frontend teams keep components pristine, stay type-safe, and ship production builds without hidden mock payloads. Maintaining a shared contract is the price of a smoother development workflow and a cleaner codebase.