Zum Inhalt springen

Supercharge Frontend Development with MSW, OpenAPI, and AI-Generated Mocks

Introduction

Modern web development calls for close interaction with backend APIs. Frontend development might be stalled while waiting for backend APIs to be ready. Mocking helps close that gap, although maintaining mocks by hand becomes difficult, especially as APIs evolve. This is where the power of the Mock Service Worker (MSW) and the OpenAPI Specification (OAS) comes into play. This post will show how to automatically create MSW handlers using an OpenAPI spec, generate mock data, and add realistic delays to the mock server.

This enables you to spin up accurate, realistic mocks with minimal manual effort and full alignment with your API contract, speeding up frontend development.

What is the OpenAPI Specification?

OpenAPI Illustration

The OpenAPI Specification (OAS) is a machine-readable contract for your API. You define everything up front: endpoints, query params, request body, responses, and data shapes instead of relying on manual communication or trial-and-error. Not only is it useful for backend developers, but it also unlocks auto-generated mock servers, documentation (like swagger), auto-generated code and more. In short, OpenAPI provides consistency, clarity, and automation to the often messy world of API development and improves communication between frontend and backend teams.

In this article, we’ll use an existing RESTful API—swapi.tech—to explore what MSW, OpenAPI, and AI can do together.

What is MSW?

Mock Service Worker (MSW) lets you intercept network requests directly in the browser or Node.js, without modifying your application code. It runs in the background (using a service worker) and responds with mock data, enabling you to build, test, and debug applications even when the backend API is unavailable or not present at all.

You can combine MSW with data generators like faker.js or rosiejs. In this post, we’ll use faker.js, as it’s the more up-to-date and actively maintained library.

Note: Before diving into setup, remember this key point from the MSW docs:

„It’s important to approach those handlers from the server’s perspective since, effectively, you are describing how the server should behave in a particular scenario.“

Even though the logic lives in your frontend codebase, think of MSW handlers as mimicking your backend logic, not extending your frontend.

🛠️ Setup

We are building a Star Wars app using data from swapi.tech, but:

  • The API has rate limits or downtime
  • You want to work offline
  • You need to test edge cases or non-existent values

Instead of recreating backend controllers, you can mock the API using MSW, generate realistic data with faker.js, and describe endpoints using an OpenAPI spec.

1. ✅ Define the OpenAPI Spec

We’ll use OpenAPI 3.1.1. First, create a folder and YAML file for your spec:

mkdir docs && touch swapi-openapi-spec.yaml
openapi: "3.1.1"
info:
  title: "swapi.tech  Star Wars API"
  version: "1.0.0"
  description: "Public read-only Star Wars data as described on swapi.tech"
servers:
  - url: https://www.swapi.tech/api

We’ll later extend this with paths and schemas. swapi.tech is simple enough to map quickly. For more complex APIs, more time is needed to fully understand relationships between resources.

🔗 Resource Endpoints

  • GET /people
  • GET /people/{id}
  • GET /planets
  • GET /planets/{id}
  • GET /starships
  • GET /starships/{id}
  • GET /films
  • GET /films/{id}
  • GET /species
  • GET /species/{id}
  • GET /vehicles
  • GET /vehicles/{id}

👨‍🚀 Entities

  • People
  • Planets
  • Films
  • Species
  • Starships
  • Vehicles

We then add schemas and query parameters like this:

paths:
  /people:
    get:
      summary: List all people
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 10
            maximum: 100
      responses:
        "200":
          description: "List of people"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PeopleListResponse"
components:
  schemas:
    BaseApiResponse:
      type: object
      properties:
        message:
          type: string
          example: "ok"
        apiVersion:
          type: string
          example: "1.0"
        timestamp:
          type: string
          format: date-time
        support:
          type: object
          properties:
            contact:
              type: string
              example: "admin@swapi.tech"
            donate:
              type: string
              example: "https://www.paypal.com/donate/..."
            partnerDiscounts:
              type: object
        social:
          type: object
          properties:
            discord:
              type: string
            reddit:
              type: string
            github:
              type: string
    PeopleListResponse:
      allOf:
        - $ref: "#/components/schemas/BaseApiResponse"
        - type: object
          properties:
            total_records:
              type: integer
            total_pages:
              type: integer
            previous:
              type: [string, "null"]
            next:
              type: [string, "null"]
            results:
              type: array
              items:
                $ref: "#/components/schemas/ListItem"
    ListItem:
      type: object
      properties:
        uid:
          type: string
        name:
          type: string
        url:
          type: string

In the above code snippet, we defined two main properties for the specification. Paths including endpoint identifiers and the actual method type (GET verb), query parameters that the endpoint can accept, and a success response (status „200“).

Another significant attribute is components which allows you to design the spec’s entities in a reusable manner and improves the spec’s readability.

💡 Tip: Use 42Crunch VS Code extension to preview your OpenAPI spec as Swagger UI.

42Crunch Example

Generating Models and Mocking

⚖️ Setup MSW

🤖 AI-Powered Models (or not)

The OpenAPI standard’s strength lies in its interoperability across various tools. So if you do not care about your models‘ privacy or OpenAPI spec privacy, you can share your OpenAPI with an AI model (ChatGPT, Claude, GitHub CoPilot, Cursor, etc.) and request it to export all models and query params from the OpenAPI spec to typescript.

Nevertheless, for enterprise-grade or schema-rich APIs, general-purpose language models are prone to hallucinations and structural inconsistencies. So instead of using a generic AI model, you can utilize deterministic generators like openapi-typescript or similar libraries that will generate the TypeScript models out of a given OpenAPI spec. Some of the benefits of using such a library are

  • Local-first model generation with no data leakage
  • Predictable and verifiable outputs
  • Open-source flexibility for introspection, extension (via forking), and community contribution

🤖 AI-Powered Mocks (or not)

Install msw-auto-mock:

npm install msw-auto-mock --save-dev

Configure your AI model in package.json:

"msw-auto-mock": {
  "ai": {
    "model": "gpt-4o",
    "enable": true,
    "provider": "openai",
    "openai": {
      "apiKey": "process.env.OPENAI_API_KEY"
    }
  }
}

Run the generator:

npx msw-auto-mock ./docs/mock-swapi-openapi.json -o ./mock

AI Mocks Output

💡 Tip: Alternatively if you don’t want to use the ai feature, you can simply by pass the configuration of msw-auto-mock in the package.json and library will generate dynamic mocks primarily based on fakerjs._

🔄 Conditionally Enable Mocking

// index.js
export async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') return;
  const { worker } = await import('./browser');
  return worker.start();
}

// main.tsx
import { enableMocking } from './mock';

enableMocking().then(() => {
  console.log('Mocking enabled');
  createRoot(document.getElementById('root')!).render(
    <StrictMode>
      <App />
    </StrictMode>
  );
});

🏢 Exclude MSW From Production Builds

Something important to consider is that we do not need the MSW for production builds; therefore, we can opt out of all MSW modules from our final bundle file. In Vite configuration, this can be done by creating a custom rollup function called manualChunks that will parse all chunk IDs, which parses all chunk IDs (where each ID is the file path). If the chunk belongs to MSW, we mark it as an external package and exclude it from the final bundled file. Finally, we add an esbuild utility to treeshake any remaining MSW initialization code for the production build.

export default defineConfig(({ command }) => {
  const isProductionBuild = process.env.NODE_ENV === "production" && command === "build";
  return {
    plugins: [react(), tsconfigPaths()],
    build: {
      target: "esnext",
      rollupOptions: {
        external: ["@faker-js/faker"],
        output: {
          ...markMSWmodulesAsExternal(isProductionBuild),
        }
      }
    },
    esbuild: {
      pure: treeShakeMSWModules(isProductionBuild)
    }
  };
});

function markMSWmodulesAsExternal(isProductionBuild = false) {
  return isProductionBuild ? {
    manualChunks(id) {
      if (id.includes("msw") || id.includes("/src/mock/")) return "msw";
    }
  } : {};
}

function treeShakeMSWModules(isProductionBuild = false) {
  return isProductionBuild ? ["startMockServiceWorker", "setupWorker", "setupServer"] : [];
}

🎨 Time to Build!

Start your app, open localhost, and start building your UI with no backend required — vibe code your Star Wars app 🤖.

Star Wars Coding Gif

MSW logs requests in the browser console:

MSW Logs

⏳ Add Delay for Realism

// handlers.js
export const handlers = [
  http.all('/*', async () => {
    await delay(2000);
  }),
  ...
];

After adding delays, your app will naturally reflect real-world latency, which is perfect for testing loading states.

Loading State

💡 Tip: To protect your frontend application from changes in backend models, use transformation methods that convert server responses to client models—and vice versa. This way, when backend data structures change, you typically only need to update your transform layer.

Example:

Initial data integration using a transform method:

type ResponseData = {
  first_name: string;
  last_name: string;
};

const transformer = (data: ResponseData) => ({
  firstName: data.first_name,
  lastName: data.last_name,
});

Later, if the backend response changes:

type ResponseData = {
  person_details: {
    first_name: string;
    last_name: string;
  };
};

// Only the transformer needs to be updated
const transformer = (data: ResponseData) => ({
  firstName: data.person_details.first_name,
  lastName: data.person_details.last_name,
});

The repository link is in the first comment ⬇️!

Thanks for reading, and may the mocks be with you! 🌟

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert