DEV Community

Cover image for 🚖 Building a Scalable Frontend Mock Architecture with MSW, Factories, and Services (Uber-Like Example)
Mohammad Rajaei Monfared
Mohammad Rajaei Monfared Subscriber

Posted on

🚖 Building a Scalable Frontend Mock Architecture with MSW, Factories, and Services (Uber-Like Example)

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 }]));
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 },
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

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(),
  }));
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
  }),
];
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Start it automatically in dev mode:

if (import.meta.env.DEV) {
  const { worker } = await import('./mocks/browser');
  worker.start({ onUnhandledRequest: 'bypass' });
}
Enter fullscreen mode Exit fullscreen mode

🧰 Bonus: Works in Tests and Storybook Too

This architecture is not just for local development — it also powers:

  • Unit/Integration Tests (via setupServer from msw/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];
Enter fullscreen mode Exit fullscreen mode

🌱 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:

🔗 Connect on LinkedIn

📝 Follow on Medium

💻 Follow on Dev.to

Top comments (0)