Skip to content
Published on

Mock Server Building Complete Guide

Authors

Overview

In modern software development, frontend and backend teams almost always work in parallel. Waiting for backend APIs to be ready before starting frontend work, or being unable to access third-party services during testing, is a constant friction point.

A Mock Server is the key tool for solving these problems. By defining the API contract first, you can develop and test the frontend without a real backend.


1. Why You Need a Mock Server

1.1 Parallel Frontend-Backend Development

Traditional approach (sequential):
  Backend API complete → Frontend development begins
  └── Delay: frontend blocked waiting for backend

With a mock server (parallel):
  API spec defined → Backend dev || Frontend dev
No blocking via mock server

Both teams can work simultaneously as long as they agree on the API contract first.

1.2 Removing External API Dependencies

Third-party services like payment APIs, mapping services, or weather APIs come with problems:

  • Costs: APIs that charge per call
  • Rate Limiting: Slows down test execution
  • Service Instability: External outages break CI/CD pipelines
  • Test Data Pollution: Test data mixing into production data

1.3 Reproducing Error Scenarios

A mock server makes it easy to reproduce situations that are hard to trigger in real services:

  • Network timeouts
  • 500 Internal Server Error
  • 429 Too Many Requests
  • Network disconnection
  • Response delays (slow network simulation)

1.4 Test Environment Stability

Eliminating external dependencies in CI/CD:
  No real DBInMemory DB Fake
  No real APIMock Server
  No real queue    → In-process Fake

2. json-server (Fastest Start)

json-server lets you create a full REST API server from a single JSON file in under 30 seconds.

2.1 Installation and Basic Usage

# Global install
npm install -g json-server

# Or as a dev dependency
npm install --save-dev json-server

2.2 db.json Structure

{
  "users": [
    { "id": 1, "name": "Alice", "email": "alice@example.com", "role": "admin" },
    { "id": 2, "name": "Bob", "email": "bob@example.com", "role": "user" }
  ],
  "posts": [
    { "id": 1, "title": "First Post", "body": "Content here", "userId": 1, "published": true },
    { "id": 2, "title": "Second Post", "body": "Content here", "userId": 2, "published": false }
  ],
  "comments": [{ "id": 1, "text": "Great post!", "postId": 1, "userId": 2 }],
  "profile": {
    "name": "Admin",
    "version": "1.0.0"
  }
}

Start json-server:

json-server --watch db.json --port 3001

Auto-generated endpoints:

GET    /users
GET    /users/1
POST   /users
PUT    /users/1
PATCH  /users/1
DELETE /users/1
GET    /posts?userId=1
GET    /posts?published=true
GET    /posts?_page=1&_limit=10
GET    /posts?_sort=title&_order=asc

2.3 Custom Routing (routes.json)

{
  "/api/*": "/$1",
  "/api/v1/users/:id": "/users/:id",
  "/blog/:id/comments": "/comments?postId=:id"
}

Apply routing on startup:

json-server --watch db.json --routes routes.json --port 3001

2.4 Adding Middleware

// middleware.js
module.exports = (req, res, next) => {
  // Check auth header
  if (req.headers.authorization !== 'Bearer valid-token') {
    if (req.method !== 'GET') {
      return res.status(401).json({ error: 'Unauthorized' })
    }
  }

  // Simulate response delay (500ms)
  setTimeout(next, 500)
}
json-server --watch db.json --middlewares middleware.js

2.5 Relational Data Modeling

json-server supports relational data via _embed and _expand:

# Get post with embedded comments
GET /posts/1?_embed=comments

# Get post with expanded user info
GET /posts?_expand=user

# Nested relationships
GET /users?_embed=posts

2.6 Pagination, Sorting, and Filtering

# Pagination (10 per page, page 2)
GET /posts?_page=2&_limit=10

# Sort by title descending
GET /posts?_sort=title&_order=desc

# Filter published posts
GET /posts?published=true

# Full-text search
GET /posts?q=search-term

# Range filter
GET /posts?id_gte=5&id_lte=10

2.7 Custom Server Script

// server.js - for finer control
const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('db.json')
const middlewares = jsonServer.defaults()

server.use(middlewares)

// Custom routes
server.post('/auth/login', (req, res) => {
  const { email, password } = req.body
  if (email === 'admin@example.com' && password === 'password') {
    res.json({ token: 'fake-jwt-token', userId: 1 })
  } else {
    res.status(401).json({ error: 'Invalid credentials' })
  }
})

server.use(router)
server.listen(3001, () => {
  console.log('JSON Server is running on port 3001')
})

3. MSW (Mock Service Worker) - The Modern Approach

MSW uses Service Workers to intercept requests at the network level. It lets you reuse the same handlers in both the browser and Node.js.

3.1 Installation and Setup

npm install msw --save-dev

# Generate Service Worker file in the public directory
npx msw init public/ --save

3.2 Writing REST API Handlers

// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  // GET list of users
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
    ])
  }),

  // GET specific user
  http.get('/api/users/:id', ({ params }) => {
    const { id } = params

    if (id === '999') {
      return new HttpResponse(null, { status: 404 })
    }

    return HttpResponse.json({
      id: Number(id),
      name: 'Alice',
      email: 'alice@example.com',
    })
  }),

  // POST create user
  http.post('/api/users', async ({ request }) => {
    const newUser = await request.json()

    return HttpResponse.json({ id: Date.now(), ...newUser }, { status: 201 })
  }),

  // PUT update user
  http.put('/api/users/:id', async ({ params, request }) => {
    const { id } = params
    const updates = await request.json()

    return HttpResponse.json({ id: Number(id), ...updates })
  }),

  // DELETE user
  http.delete('/api/users/:id', ({ params }) => {
    return new HttpResponse(null, { status: 204 })
  }),
]

3.3 GraphQL Handlers

import { graphql, HttpResponse } from 'msw'

export const graphqlHandlers = [
  graphql.query('GetUser', ({ variables }) => {
    const { id } = variables

    return HttpResponse.json({
      data: {
        user: {
          id,
          name: 'Alice',
          email: 'alice@example.com',
          posts: [{ id: 1, title: 'First Post' }],
        },
      },
    })
  }),

  graphql.mutation('CreateUser', ({ variables }) => {
    const { name, email } = variables

    return HttpResponse.json({
      data: {
        createUser: {
          id: String(Date.now()),
          name,
          email,
        },
      },
    })
  }),
]

3.4 Dynamic Responses Using Request Parameters

import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/products', ({ request }) => {
    const url = new URL(request.url)
    const category = url.searchParams.get('category')
    const page = Number(url.searchParams.get('page') ?? '1')
    const limit = Number(url.searchParams.get('limit') ?? '10')

    const allProducts = [
      { id: 1, name: 'Laptop', category: 'electronics', price: 1200 },
      { id: 2, name: 'Mouse', category: 'electronics', price: 30 },
      { id: 3, name: 'Desk', category: 'furniture', price: 250 },
    ]

    let filtered = allProducts
    if (category) {
      filtered = allProducts.filter((p) => p.category === category)
    }

    const start = (page - 1) * limit
    const data = filtered.slice(start, start + limit)

    return HttpResponse.json({
      data,
      total: filtered.length,
      page,
      limit,
    })
  }),
]

3.5 Error Response Scenarios

import { http, HttpResponse } from 'msw'

// Pattern to dynamically control error scenarios
let shouldFailNext = false

export const handlers = [
  // Test-only control endpoint
  http.post('/test/simulate-error', async ({ request }) => {
    const { fail } = await request.json()
    shouldFailNext = fail
    return new HttpResponse(null, { status: 200 })
  }),

  http.post('/api/payment', async ({ request }) => {
    if (shouldFailNext) {
      shouldFailNext = false
      return HttpResponse.json(
        { error: 'PAYMENT_DECLINED', message: 'Card was declined' },
        { status: 402 }
      )
    }

    const { amount } = await request.json()
    return HttpResponse.json({
      paymentId: `PAY-${Date.now()}`,
      amount,
      status: 'SUCCESS',
    })
  }),

  // Network error simulation
  http.get('/api/unstable', () => {
    if (Math.random() > 0.7) {
      return HttpResponse.error()
    }
    return HttpResponse.json({ data: 'success' })
  }),
]

3.6 Browser Environment Setup

// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)
// src/main.tsx (React entry point)
async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') {
    return
  }

  const { worker } = await import('./mocks/browser')
  return worker.start({
    onUnhandledRequest: 'bypass' // Pass through unhandled requests
  })
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  )
})

3.7 Node.js Environment Setup (for Tests)

// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
// jest.setup.ts
import { server } from './src/mocks/server'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// Override handlers in specific tests
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'

test('handles API error gracefully', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Server Error' }, { status: 500 })
    })
  )

  const result = await fetchUsers()
  expect(result.error).toBe('Server Error')
})

3.8 MSW in Next.js

// next.config.js
const nextConfig = {
  async headers() {
    return [
      {
        source: '/mockServiceWorker.js',
        headers: [{ key: 'Service-Worker-Allowed', value: '/' }],
      },
    ]
  },
}
// src/app/providers.tsx (App Router)
'use client'

import { useEffect } from 'react'

export function MSWProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
      import('../mocks/browser').then(({ worker }) => {
        worker.start({ onUnhandledRequest: 'bypass' })
      })
    }
  }, [])

  return children
}

4. WireMock - Java/JVM Environment

WireMock is the most widely used HTTP mock server in the Java/JVM ecosystem.

4.1 Running WireMock Standalone

# Download WireMock
curl -o wiremock.jar https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.35.0/wiremock-jre8-standalone-2.35.0.jar

# Run
java -jar wiremock.jar --port 8080 --verbose

Directory structure:

wiremock/
├── mappings/          # Stub definition JSON files
│   ├── users.json
│   └── orders.json
└── __files/           # Response body files
    ├── users.json
    └── order-template.json

4.2 Defining Stubs with JSON Files

{
  "request": {
    "method": "GET",
    "url": "/api/users"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": [
      { "id": 1, "name": "Alice", "email": "alice@example.com" },
      { "id": 2, "name": "Bob", "email": "bob@example.com" }
    ]
  }
}

4.3 Request Matching (URL, Headers, Body)

{
  "request": {
    "method": "POST",
    "url": "/api/orders",
    "headers": {
      "Content-Type": {
        "equalTo": "application/json"
      },
      "Authorization": {
        "matches": "Bearer .+"
      }
    },
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.items[*].productId"
      },
      {
        "equalToJson": {
          "userId": 1
        },
        "ignoreArrayOrder": true,
        "ignoreExtraElements": true
      }
    ]
  },
  "response": {
    "status": 201,
    "jsonBody": {
      "orderId": "ORDER-001",
      "status": "PENDING"
    }
  }
}

4.4 Response Templating (Handlebars)

{
  "request": {
    "method": "GET",
    "urlPathPattern": "/api/users/([0-9]+)"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": "{ \"id\": {{request.pathSegments.[2]}}, \"name\": \"User-{{request.pathSegments.[2]}}\", \"timestamp\": \"{{now}}\" }",
    "transformers": ["response-template"]
  }
}

4.5 Scenarios (State-Based Mocking)

Scenarios let you return different responses based on current state:

{
  "scenarioName": "Order Processing Flow",
  "requiredScenarioState": "Started",
  "newScenarioState": "Payment Complete",
  "request": {
    "method": "POST",
    "url": "/api/orders/pay"
  },
  "response": {
    "status": 200,
    "jsonBody": { "status": "PAID" }
  }
}
{
  "scenarioName": "Order Processing Flow",
  "requiredScenarioState": "Payment Complete",
  "newScenarioState": "Shipping",
  "request": {
    "method": "GET",
    "url": "/api/orders/ORDER-001"
  },
  "response": {
    "status": 200,
    "jsonBody": { "status": "SHIPPING" }
  }
}

4.6 Record and Playback

Record real API responses and replay them:

# Run in record mode (proxy to real API)
java -jar wiremock.jar --proxy-all="https://api.example.com" --record-mappings --port 8080

# Replay recorded responses (default mode)
java -jar wiremock.jar --port 8080

5. Prism - OpenAPI-Based Mock Server

Prism automatically generates a mock server from an OpenAPI (Swagger) specification.

5.1 Installation and Usage

npm install -g @stoplight/prism-cli

# Run a mock server from an OpenAPI spec
npx @stoplight/prism-cli mock api.yaml --port 4010

5.2 OpenAPI Spec Example

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      operationId: getUsers
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
              examples:
                default:
                  value:
                    - id: 1
                      name: Alice
                      email: alice@example.com
    post:
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
    CreateUserRequest:
      type: object
      required: [name, email]
      properties:
        name:
          type: string
        email:
          type: string
          format: email

5.3 Dynamic Response Generation

Prism generates values automatically from schemas when no example is provided:

# Dynamic mode (schema-based random responses)
npx @stoplight/prism-cli mock api.yaml --dynamic

5.4 Validation Mode

# Validation mode - validates requests and responses against the spec
npx @stoplight/prism-cli proxy api.yaml https://api.example.com --validate-request --validate-response

6. Building a Mock Server in Go

The Go standard library is sufficient to build a complete mock server.

6.1 Core Mock Server Structure

package mockserver

import (
    "encoding/json"
    "log"
    "net/http"
    "sync"
    "time"
)

type MockServer struct {
    server *http.Server
    mux    *http.ServeMux
    mu     sync.RWMutex
    stubs  map[string]*Stub
    calls  []RequestLog
}

type Stub struct {
    Method     string
    Path       string
    StatusCode int
    Body       interface{}
    Headers    map[string]string
    Delay      time.Duration
}

type RequestLog struct {
    Method    string
    Path      string
    Headers   map[string][]string
    Body      []byte
    Timestamp time.Time
}

func NewMockServer(port string) *MockServer {
    ms := &MockServer{
        mux:   http.NewServeMux(),
        stubs: make(map[string]*Stub),
    }

    ms.server = &http.Server{
        Addr:    ":" + port,
        Handler: ms.mux,
    }

    ms.mux.HandleFunc("/", ms.handleRequest)

    // Admin control endpoints
    ms.mux.HandleFunc("/__admin/stubs", ms.handleAdminStubs)
    ms.mux.HandleFunc("/__admin/calls", ms.handleAdminCalls)
    ms.mux.HandleFunc("/__admin/reset", ms.handleAdminReset)

    return ms
}

func (ms *MockServer) Start() {
    go func() {
        log.Printf("MockServer starting on %s", ms.server.Addr)
        if err := ms.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("MockServer failed: %v", err)
        }
    }()
    time.Sleep(100 * time.Millisecond)
}

func (ms *MockServer) Stop() {
    ms.server.Close()
}

func (ms *MockServer) AddStub(stub *Stub) {
    ms.mu.Lock()
    defer ms.mu.Unlock()
    key := stub.Method + ":" + stub.Path
    ms.stubs[key] = stub
}

func (ms *MockServer) handleRequest(w http.ResponseWriter, r *http.Request) {
    // Log the request
    ms.mu.Lock()
    ms.calls = append(ms.calls, RequestLog{
        Method:    r.Method,
        Path:      r.URL.Path,
        Headers:   r.Header,
        Timestamp: time.Now(),
    })
    ms.mu.Unlock()

    // Look up stub
    ms.mu.RLock()
    key := r.Method + ":" + r.URL.Path
    stub, ok := ms.stubs[key]
    ms.mu.RUnlock()

    if !ok {
        http.Error(w, "No stub found for "+r.Method+" "+r.URL.Path, http.StatusNotFound)
        return
    }

    // Simulate response delay
    if stub.Delay > 0 {
        time.Sleep(stub.Delay)
    }

    // Set headers
    for k, v := range stub.Headers {
        w.Header().Set(k, v)
    }
    if w.Header().Get("Content-Type") == "" {
        w.Header().Set("Content-Type", "application/json")
    }

    w.WriteHeader(stub.StatusCode)

    if stub.Body != nil {
        json.NewEncoder(w).Encode(stub.Body)
    }
}

func (ms *MockServer) handleAdminReset(w http.ResponseWriter, r *http.Request) {
    ms.mu.Lock()
    ms.stubs = make(map[string]*Stub)
    ms.calls = nil
    ms.mu.Unlock()
    w.WriteHeader(http.StatusOK)
}

func (ms *MockServer) handleAdminCalls(w http.ResponseWriter, r *http.Request) {
    ms.mu.RLock()
    defer ms.mu.RUnlock()
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(ms.calls)
}

func (ms *MockServer) handleAdminStubs(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        var stub Stub
        if err := json.NewDecoder(r.Body).Decode(&stub); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        ms.AddStub(&stub)
        w.WriteHeader(http.StatusCreated)
        return
    }

    ms.mu.RLock()
    defer ms.mu.RUnlock()
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(ms.stubs)
}

6.2 Using the Mock Server in Tests

func TestUserService_GetUser(t *testing.T) {
    ms := NewMockServer("9999")
    ms.Start()
    defer ms.Stop()

    ms.AddStub(&Stub{
        Method:     "GET",
        Path:       "/api/users/1",
        StatusCode: 200,
        Body: map[string]interface{}{
            "id":    1,
            "name":  "Alice",
            "email": "alice@example.com",
        },
    })

    client := NewUserServiceClient("http://localhost:9999")
    user, err := client.GetUser(1)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}

func TestUserService_GetUser_NotFound(t *testing.T) {
    ms := NewMockServer("9998")
    ms.Start()
    defer ms.Stop()

    ms.AddStub(&Stub{
        Method:     "GET",
        Path:       "/api/users/999",
        StatusCode: 404,
        Body:       map[string]string{"error": "User not found"},
    })

    client := NewUserServiceClient("http://localhost:9998")
    _, err := client.GetUser(999)

    if err == nil {
        t.Error("expected error, got nil")
    }
}

7. Building a Mock Server in TypeScript/Node.js

7.1 Express.js + TypeScript Mock Server

// mock-server/index.ts
import express, { Request, Response, NextFunction } from 'express'
import fs from 'fs'
import path from 'path'

interface StubDefinition {
  method: string
  path: string
  statusCode: number
  body?: unknown
  headers?: Record<string, string>
  delay?: number
}

const app = express()
app.use(express.json())

const stubs: Map<string, StubDefinition> = new Map()
const requestLog: Array<{ method: string; path: string; timestamp: string }> = []

// Request logging middleware
app.use((req: Request, res: Response, next: NextFunction) => {
  requestLog.push({
    method: req.method,
    path: req.path,
    timestamp: new Date().toISOString(),
  })
  next()
})

// Admin API
app.post('/__admin/stubs', (req: Request, res: Response) => {
  const stub: StubDefinition = req.body
  const key = `${stub.method}:${stub.path}`
  stubs.set(key, stub)
  res.status(201).json({ message: 'Stub registered', key })
})

app.delete('/__admin/stubs', (req: Request, res: Response) => {
  stubs.clear()
  requestLog.length = 0
  res.status(200).json({ message: 'All stubs cleared' })
})

app.get('/__admin/calls', (req: Request, res: Response) => {
  res.json(requestLog)
})

// Dynamic response handler
app.use(async (req: Request, res: Response) => {
  const key = `${req.method}:${req.path}`
  const stub = stubs.get(key)

  if (!stub) {
    return res.status(404).json({
      error: 'No stub found',
      request: { method: req.method, path: req.path },
    })
  }

  // Response delay
  if (stub.delay && stub.delay > 0) {
    await new Promise((resolve) => setTimeout(resolve, stub.delay))
  }

  // Set headers
  if (stub.headers) {
    Object.entries(stub.headers).forEach(([key, value]) => {
      res.setHeader(key, value)
    })
  }

  res.status(stub.statusCode).json(stub.body)
})

const PORT = process.env.MOCK_PORT ?? '3001'
app.listen(PORT, () => {
  console.log(`Mock server running on port ${PORT}`)
})

export { app }

7.2 File-Based Response Routing

// Manage responses via directory structure
// responses/
//   GET/
//     api/
//       users/
//         index.json    -> GET /api/users
//         1.json        -> GET /api/users/1
//   POST/
//     api/
//       users/
//         index.json    -> POST /api/users (response template)

app.use((req: Request, res: Response) => {
  const responsePath = path.join(__dirname, 'responses', req.method, req.path, 'index.json')

  if (fs.existsSync(responsePath)) {
    const responseData = JSON.parse(fs.readFileSync(responsePath, 'utf-8'))
    res.json(responseData)
  } else {
    res.status(404).json({ error: 'No response file found' })
  }
})

7.3 Middleware for Delay and Error Injection

// chaos-middleware.ts - middleware for chaos engineering
interface ChaosConfig {
  errorRate: number // 0.0 to 1.0
  minDelay: number // ms
  maxDelay: number // ms
}

let chaosConfig: ChaosConfig = {
  errorRate: 0,
  minDelay: 0,
  maxDelay: 0,
}

export const chaosMiddleware = async (req: Request, res: Response, next: NextFunction) => {
  // Response delay
  const delay = chaosConfig.minDelay + Math.random() * (chaosConfig.maxDelay - chaosConfig.minDelay)
  if (delay > 0) {
    await new Promise((resolve) => setTimeout(resolve, delay))
  }

  // Random error injection
  if (Math.random() < chaosConfig.errorRate) {
    return res.status(500).json({
      error: 'Chaos monkey strikes!',
      timestamp: new Date().toISOString(),
    })
  }

  next()
}

// Config update API
app.post('/__admin/chaos', (req: Request, res: Response) => {
  chaosConfig = { ...chaosConfig, ...req.body }
  res.json({ message: 'Chaos config updated', config: chaosConfig })
})

8. Docker Compose Environment for Mock Servers

8.1 docker-compose.yml

version: '3.8'

services:
  # json-server mock
  json-server:
    image: clue/json-server
    volumes:
      - ./mock-data/db.json:/data/db.json
      - ./mock-data/routes.json:/data/routes.json
    ports:
      - '3001:80'
    command: --watch db.json --routes routes.json
    healthcheck:
      test: ['CMD', 'wget', '-qO-', 'http://localhost/users']
      interval: 10s
      timeout: 5s
      retries: 3

  # WireMock server
  wiremock:
    image: wiremock/wiremock:latest
    ports:
      - '8080:8080'
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings
      - ./wiremock/__files:/home/wiremock/__files
    command:
      - '--verbose'
      - '--global-response-templating'
    healthcheck:
      test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/__admin/health']
      interval: 10s
      timeout: 5s
      retries: 3

  # Application (uses mock servers as dependencies)
  app:
    build: .
    ports:
      - '8000:8000'
    environment:
      - USER_SERVICE_URL=http://json-server:80
      - PAYMENT_SERVICE_URL=http://wiremock:8080
    depends_on:
      json-server:
        condition: service_healthy
      wiremock:
        condition: service_healthy

  # E2E test runner
  e2e-tests:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      - APP_URL=http://app:8000
    depends_on:
      - app
    command: npm run test:e2e
    profiles:
      - test

8.2 Test Execution Script

#!/bin/bash
# run-tests.sh

echo "Starting mock server environment..."
docker-compose up -d json-server wiremock

echo "Waiting for health checks..."
docker-compose exec json-server wget -qO- http://localhost/users
docker-compose exec wiremock wget -qO- http://localhost:8080/__admin/health

echo "Starting application..."
docker-compose up -d app

echo "Running E2E tests..."
docker-compose run --rm e2e-tests

echo "Cleaning up..."
docker-compose down

8.3 Mock Data Initialization Script

// scripts/setup-mock-data.ts
const WIREMOCK_URL = 'http://localhost:8080/__admin/mappings'

const stubs = [
  {
    request: {
      method: 'POST',
      url: '/api/payment/charge',
    },
    response: {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
      jsonBody: {
        paymentId: 'TEST-PAYMENT-001',
        status: 'SUCCESS',
        amount: 100,
      },
    },
  },
  {
    request: {
      method: 'POST',
      url: '/api/payment/charge',
      headers: {
        'X-Simulate-Failure': { equalTo: 'true' },
      },
    },
    response: {
      status: 402,
      headers: { 'Content-Type': 'application/json' },
      jsonBody: {
        error: 'PAYMENT_DECLINED',
        message: 'Card was declined',
      },
    },
  },
]

async function setupMockData() {
  for (const stub of stubs) {
    const response = await fetch(WIREMOCK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(stub),
    })
    console.log(`Stub registered: ${response.status}`)
  }
}

setupMockData().catch(console.error)

9. Quiz

Quiz 1: What is the key difference between MSW and other mocking approaches like axios-mock-adapter?

Answer: MSW uses a Service Worker to intercept requests at the network level, so it works regardless of which HTTP library the application uses. You can also reuse the same handlers in both the browser and Node.js.

Explanation: Libraries like axios-mock-adapter mock the HTTP client itself, so changing the HTTP client requires rewriting your mock code. MSW intercepts at the actual network layer, meaning it works with fetch, axios, or any other HTTP client transparently. This also makes your mocks more realistic because the full HTTP stack is exercised.

Quiz 2: What is the main limitation of json-server, and how can you overcome it?

Answer: The main limitation of json-server is that it is difficult to implement complex business logic beyond simple CRUD operations.

Explanation: To overcome this, you can use json-server as a library and attach custom routes (server.post('/auth/login', ...)), or write custom middleware to handle more complex logic. For significantly complex scenarios, switching to WireMock or a custom-built mock server is a better approach.

Quiz 3: In what situations is WireMock's Scenarios feature most useful?

Answer: Scenarios are most useful for testing workflows where different responses should be returned based on current state — for example, an order status flow (payment pending → paid → shipping → delivered) or retry logic (first request fails, second succeeds).

Explanation: A standard stub always returns the same response. With Scenarios, each incoming request transitions the state machine to the next state and returns a response appropriate for that state. This is essential for testing non-idempotent APIs and stateful workflows.

Quiz 4: What is the primary advantage of using Prism?

Answer: Prism's main advantage is that it automatically generates a mock server from an OpenAPI spec with no additional mocking code. In Validation mode, it also verifies that real API requests and responses conform to the spec.

Explanation: Prism is particularly valuable in an API-First Design workflow. Once the API spec is defined, the frontend can immediately start development using Prism, and once the backend is complete, Validation mode can automatically verify spec compliance. This eliminates spec drift between documentation and implementation.

Quiz 5: Why should mock servers be combined with Contract Testing?

Answer: Mock servers can drift out of sync with real APIs. Combining with Contract Testing automatically verifies that the mock server matches the real API, preventing bugs caused by mismatches between the mock and the real service.

Explanation: If you develop against a mock server but the real API changes, tests pass in development but fail in production. Tools like Pact let the consumer (frontend) define its expectations as a contract, and the provider (backend) automatically verifies it satisfies those expectations. This creates a safety net that catches integration bugs before deployment.