If you’ve ever built an app that depends on complex backend APIs — like a ride-sharing or delivery platform — you know the pain:
“The backend isn’t ready yet… but I still need to test my UI, maps, and user flows.”
Frontend developers often get stuck waiting for endpoints or using rigid JSON mocks that quickly become outdated.
There’s a better way.
In this article, we’ll explore how to build a scalable mock architecture for your frontend using Mock Service Worker (MSW) — the modern way to simulate APIs directly in the browser or test environment.
We’ll model a simplified Uber-like app with entities like User
, Drivers
, Vehicles
, Wallet
, MyLastTrips
, and more.
🧠 Note: The entities and examples here are simplified to illustrate the architecture.
Real ride-sharing systems have more complex relationships and data flows.
🧩 Why Traditional Mocks Don’t Scale
Most frontend teams start with something simple:
// mocks/handlers.ts
rest.get('/api/drivers', (req, res, ctx) => {
return res(ctx.json([{ id: 1, name: 'John', rating: 4.9 }]));
});
It works… until it doesn’t.
When your app grows — filters, pagination, dynamic routes, price estimates, and user-specific logic — this flat mock structure becomes unmaintainable.
The solution?
Design your mocks like a real backend, with clear layers and responsibilities.
🏗 The Layered Mock Architecture
Here’s the folder structure we’ll use:
src/
mocks/
handlers/
driversHandlers.ts
ridesHandlers.ts
factories/
userFactory.ts
driversFactory.ts
vehiclesFactory.ts
walletFactory.ts
myLocationsFactory.ts
myLastTripsFactory.ts
generators/
carLocationsGenerator.ts
rideRequestsGenerator.ts
priceEstimatesGenerator.ts
routesGenerator.ts
services/
ridesService.ts
driversService.ts
walletService.ts
browser.ts
server.ts
Each layer has a specific purpose:
Layer | Role | Description |
---|---|---|
factories/ |
Static seed data | Acts as your “mock database” |
generators/ |
Dynamic or computed data | Produces live, random, or rule-based data |
services/ |
Business logic | Simulates backend operations (search, filters, etc.) |
handlers/ |
API interface | Connects MSW routes to your mock services |
🧱 Step 1: Factories — The Mock Database
Factories provide static, consistent seed data.
They act like an in-memory database for your mocks.
Here’s a reusable base factory:
// factories/BaseFactory.ts
export class BaseFactory {
constructor(seed = []) {
this.seed = seed;
}
clone(value) {
return typeof structuredClone === 'function'
? structuredClone(value)
: JSON.parse(JSON.stringify(value));
}
createMany() {
return this.seed.map((item) => this.clone(item));
}
createOne() {
if (!this.seed.length)
throw new Error(`${this.constructor.name} has no seed data`);
return this.clone(this.seed[0]);
}
}
Then, define your domain factories:
// factories/driversFactory.ts
import { BaseFactory } from './BaseFactory';
export class DriversFactory extends BaseFactory {
constructor() {
super([
{ id: 1, name: 'Alice', vehicleId: 101, rating: 4.9 },
{ id: 2, name: 'Bob', vehicleId: 102, rating: 4.7 },
]);
}
}
Other factories might include:
-
UserFactory
– the logged-in user data -
VehiclesFactory
– registered vehicles -
WalletFactory
– balance and transactions -
MyLocationsFactory
– user’s saved addresses -
MyLastTripsFactory
– trip history
✅ Tip: Keep this data simple and clean. It doesn’t need to be “real” — just realistic.
⚙️ Step 2: Generators — Dynamic or Computed Data
Some data can’t be static. Think about:
- A driver’s live location
- Price estimates that change with distance
- Routes generated from coordinates
That’s where generators come in.
// generators/carLocationsGenerator.ts
import { faker } from '@faker-js/faker';
export function generateCarLocations(count = 5) {
return Array.from({ length: count }, () => ({
driverId: faker.number.int({ min: 1, max: 5 }),
lat: faker.location.latitude(),
lng: faker.location.longitude(),
updatedAt: faker.date.recent().toISOString(),
}));
}
You can build similar ones for:
-
rideRequestsGenerator
– mock incoming ride requests -
priceEstimatesGenerator
– calculate approximate fares -
routesGenerator
– fake polyline routes between points
✅ Why it matters:
Generators make your mocks feel alive, perfect for testing live maps, dynamic UI updates, or ride-status screens.
🧠 Step 3: Services — Mock Backend Logic
Services combine factories and generators into domain-specific logic.
// services/ridesService.ts
import { MyLastTripsFactory } from '../factories/myLastTripsFactory';
import { generateRideRequests } from '../generators/rideRequestsGenerator';
import { generatePriceEstimates } from '../generators/priceEstimatesGenerator';
export class RidesService {
constructor() {
this.trips = new MyLastTripsFactory().createMany();
}
listMyTrips(userId) {
return this.trips.filter((trip) => trip.userId === userId);
}
getActiveRideRequests() {
return generateRideRequests(3);
}
getPriceEstimate(pickup, dropoff) {
return generatePriceEstimates(pickup, dropoff);
}
}
✅ Why it matters:
This mirrors how a real backend behaves — filtering, calculating, and combining data logically — not just serving static JSON.
🌐 Step 4: Handlers — The MSW API Layer
Now we wire everything together using Mock Service Worker (MSW):
// handlers/ridesHandlers.ts
import { rest } from 'msw';
import { RidesService } from '../services/ridesService';
const ridesService = new RidesService();
export const ridesHandlers = [
rest.get('/api/trips', (req, res, ctx) => {
const userId = Number(req.url.searchParams.get('userId') || 1);
const trips = ridesService.listMyTrips(userId);
return res(ctx.status(200), ctx.json(trips));
}),
rest.get('/api/price-estimate', (req, res, ctx) => {
const pickup = req.url.searchParams.get('pickup');
const dropoff = req.url.searchParams.get('dropoff');
const price = ridesService.getPriceEstimate(pickup, dropoff);
return res(ctx.status(200), ctx.json(price));
}),
];
And connect it to your dev environment:
// mocks/browser.ts
import { setupWorker } from 'msw';
import { ridesHandlers } from './handlers/ridesHandlers';
import { driversHandlers } from './handlers/driversHandlers';
export const worker = setupWorker(...ridesHandlers, ...driversHandlers);
Start it automatically in dev mode:
if (import.meta.env.DEV) {
const { worker } = await import('./mocks/browser');
worker.start({ onUnhandledRequest: 'bypass' });
}
🧰 Bonus: Works in Tests and Storybook Too
This architecture is not just for local development — it also powers:
-
Unit/Integration Tests (via
setupServer
frommsw/node
) -
Storybook (via
msw-storybook-addon
) - End-to-End Prototyping without a real backend
Example Storybook config:
import { initialize, mswDecorator } from 'msw-storybook-addon';
initialize();
export const decorators = [mswDecorator];
🌱 Why This Architecture Works
Benefit | Description |
---|---|
🧩 Separation of Concerns | Each layer (factory, generator, service) does one job |
⚙️ Backend-Like Simulation | Services handle filtering, routes, and price logic |
🔁 Reusability | Works in dev, tests, and Storybook |
💡 Flexibility | Add or override scenarios easily (e.g., surge pricing, no drivers) |
🧱 Maintainability | Add new entities (e.g., Wallet, Vehicles) without rewriting mocks |
✅ Conclusion
This architecture gives you production-like realism without depending on real APIs.
Your frontend becomes faster to develop, test, and demo — even before the backend exists.
By organizing your mocks into:
- 🧱 Factories (static seeds)
- ⚙️ Generators (dynamic data)
- 🧠 Services (logic layer)
…and connecting them through MSW handlers, you create a self-contained virtual backend for your app.
This example used Uber-like entities such as Drivers, Vehicles, MyLocations, Wallet, and RideRequests, but the same pattern applies to any complex domain — e-commerce, fintech, IoT, you name it.
Click the links below to follow me on all platforms:
Top comments (0)