API Packages@mockmaster/msw-adapter

@mockmaster/msw-adapter

Record & replay engine

npm install @mockmaster/msw-adapter

Overview

The MSW adapter package provides the core record and replay functionality for MockMaster.

Key Features:

  • Create scenarios and recordings
  • Replay handlers for request matching
  • Request/response pair management
  • Type-safe recording creation
  • Deterministic response generation

API Reference

createScenario(name, description)

Create an empty scenario.

import { createScenario } from '@mockmaster/msw-adapter'
 
const scenario = createScenario('user-api', 'User API endpoints')
 
console.log(scenario)
// {
//   name: 'user-api',
//   description: 'User API endpoints',
//   recordings: [],
//   createdAt: 1234567890,
//   updatedAt: 1234567890
// }

Parameters:

  • name (string) - Scenario name
  • description (string) - Scenario description

Returns: Scenario object

createRecording(request, response)

Create a recording of a request/response pair.

import { createRecording } from '@mockmaster/msw-adapter'
 
const recording = createRecording(
  {
    method: 'GET',
    url: 'https://api.example.com/users/123',
    path: '/users/:id',
    timestamp: Date.now()
  },
  {
    status: 200,
    body: { id: 123, name: 'John Doe' },
    headers: { 'content-type': 'application/json' },
    timestamp: Date.now()
  }
)

Parameters:

  • request (object) - Request details
  • response (object) - Response details

Returns: Recording object

Request Properties:

  • method (string) - HTTP method
  • url (string) - Full URL
  • path (string) - Path portion (can include parameters)
  • timestamp (number) - Request timestamp
  • body (any, optional) - Request body
  • headers (object, optional) - Request headers

Response Properties:

  • status (number) - HTTP status code
  • body (any) - Response body
  • timestamp (number) - Response timestamp
  • headers (object, optional) - Response headers

addRecordingToScenario(scenario, recording)

Add a recording to a scenario.

import { addRecordingToScenario } from '@mockmaster/msw-adapter'
 
let scenario = createScenario('api', 'API endpoints')
 
const recording = createRecording(
  { method: 'GET', url: 'https://api.example.com/users', path: '/users', timestamp: Date.now() },
  { status: 200, body: [], timestamp: Date.now() }
)
 
scenario = addRecordingToScenario(scenario, recording)
 
console.log(scenario.recordings.length)  // 1

Parameters:

  • scenario (Scenario) - Scenario to add to
  • recording (Recording) - Recording to add

Returns: New scenario with recording added

createReplayHandler(scenario)

Create a replay handler from a scenario.

import { createReplayHandler } from '@mockmaster/msw-adapter'
 
const handler = createReplayHandler(scenario)
 
// Replay requests
const response = handler({
  method: 'GET',
  path: '/users/123'
})
 
console.log(response.status)  // 200
console.log(response.body)    // { id: 123, name: 'John Doe' }

Parameters:

  • scenario (Scenario) - Scenario to create handler from

Returns: Handler function

Handler Function:

  • Takes request object: { method: string, path: string, body?: any }
  • Returns response object or undefined if no match

Types

Scenario

interface Scenario {
  name: string
  description: string
  recordings: Recording[]
  createdAt: number
  updatedAt: number
}

Recording

interface Recording {
  request: {
    method: string
    url: string
    path: string
    timestamp: number
    body?: any
    headers?: Record<string, string>
  }
  response: {
    status: number
    body: any
    timestamp: number
    headers?: Record<string, string>
  }
}

ReplayHandler

type ReplayHandler = (request: {
  method: string
  path: string
  body?: any
}) => {
  status: number
  body: any
  headers?: Record<string, string>
} | undefined

Usage Examples

Basic Recording

import {
  createScenario,
  createRecording,
  addRecordingToScenario,
  createReplayHandler
} from '@mockmaster/msw-adapter'
 
// Create scenario
let scenario = createScenario('user-api', 'User API operations')
 
// Create recording
const getUserRecording = createRecording(
  {
    method: 'GET',
    url: 'https://api.example.com/users/1',
    path: '/users/:id',
    timestamp: Date.now()
  },
  {
    status: 200,
    body: {
      id: 1,
      name: 'John Doe',
      email: 'john@example.com'
    },
    headers: {
      'content-type': 'application/json'
    },
    timestamp: Date.now()
  }
)
 
// Add to scenario
scenario = addRecordingToScenario(scenario, getUserRecording)
 
// Create handler
const handler = createReplayHandler(scenario)
 
// Use in test
const response = handler({ method: 'GET', path: '/users/1' })
expect(response.status).toBe(200)
expect(response.body.name).toBe('John Doe')

Multiple Recordings

let scenario = createScenario('rest-api', 'Complete REST API')
 
// GET /users
const listRecording = createRecording(
  { method: 'GET', url: 'https://api.example.com/users', path: '/users', timestamp: Date.now() },
  { status: 200, body: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }], timestamp: Date.now() }
)
scenario = addRecordingToScenario(scenario, listRecording)
 
// GET /users/:id
const getRecording = createRecording(
  { method: 'GET', url: 'https://api.example.com/users/1', path: '/users/:id', timestamp: Date.now() },
  { status: 200, body: { id: 1, name: 'Alice' }, timestamp: Date.now() }
)
scenario = addRecordingToScenario(scenario, getRecording)
 
// POST /users
const createRecording = createRecording(
  {
    method: 'POST',
    url: 'https://api.example.com/users',
    path: '/users',
    body: { name: 'Charlie' },
    timestamp: Date.now()
  },
  { status: 201, body: { id: 3, name: 'Charlie' }, timestamp: Date.now() }
)
scenario = addRecordingToScenario(scenario, createRecording)
 
// Use handler
const handler = createReplayHandler(scenario)
 
handler({ method: 'GET', path: '/users' })     // List users
handler({ method: 'GET', path: '/users/1' })   // Get user 1
handler({ method: 'POST', path: '/users' })    // Create user

Error Responses

// 404 Not Found
const notFoundRecording = createRecording(
  { method: 'GET', url: 'https://api.example.com/users/999', path: '/users/:id', timestamp: Date.now() },
  {
    status: 404,
    body: {
      error: 'Not Found',
      message: 'User with ID 999 not found'
    },
    timestamp: Date.now()
  }
)
 
// 400 Bad Request
const badRequestRecording = createRecording(
  {
    method: 'POST',
    url: 'https://api.example.com/users',
    path: '/users',
    body: { email: 'invalid' },
    timestamp: Date.now()
  },
  {
    status: 400,
    body: {
      error: 'Bad Request',
      errors: [
        { field: 'email', message: 'Invalid email format' }
      ]
    },
    timestamp: Date.now()
  }
)
 
// 500 Internal Server Error
const serverErrorRecording = createRecording(
  { method: 'GET', url: 'https://api.example.com/error', path: '/error', timestamp: Date.now() },
  {
    status: 500,
    body: {
      error: 'Internal Server Error',
      errorId: 'err_12345'
    },
    timestamp: Date.now()
  }
)

Type-Safe Recordings

interface User {
  id: number
  name: string
  email: string
}
 
const recording = createRecording<User>(
  {
    method: 'GET',
    url: 'https://api.example.com/users/1',
    path: '/users/:id',
    timestamp: Date.now()
  },
  {
    status: 200,
    body: {
      id: 1,
      name: 'John Doe',
      email: 'john@example.com'
    },
    timestamp: Date.now()
  }
)
 
// TypeScript knows response.body is User type
const handler = createReplayHandler(scenario)
const response = handler({ method: 'GET', path: '/users/1' })
if (response) {
  const user: User = response.body  // Type-safe!
}

Request Matching

The replay handler matches requests using:

Method Matching

handler({ method: 'GET', path: '/users' })     // Only matches GET
handler({ method: 'POST', path: '/users' })    // Only matches POST

Path Matching

Uses @mockmaster/core for path matching:

// Exact match
handler({ method: 'GET', path: '/users' })
 
// Parameter match
handler({ method: 'GET', path: '/users/123' })  // Matches '/users/:id'
 
// Wildcard match
handler({ method: 'GET', path: '/api/anything' })  // Matches '/api/*'

No Match

Returns undefined when no recording matches:

const response = handler({ method: 'GET', path: '/unknown' })
 
if (!response) {
  console.log('No recording found')
}

Integration with Tests

Vitest

import { describe, it, expect, beforeEach } from 'vitest'
import { createReplayHandler } from '@mockmaster/msw-adapter'
import { readScenario } from '@mockmaster/cli'
 
describe('User API', () => {
  let handler: ReturnType<typeof createReplayHandler>
 
  beforeEach(async () => {
    const scenario = await readScenario('./scenarios', 'user-api')
    handler = createReplayHandler(scenario)
  })
 
  it('fetches user by ID', () => {
    const response = handler({ method: 'GET', path: '/users/1' })
 
    expect(response).toBeDefined()
    expect(response!.status).toBe(200)
    expect(response!.body).toMatchObject({
      id: 1,
      name: expect.any(String)
    })
  })
})

Jest

describe('User API', () => {
  let handler: ReturnType<typeof createReplayHandler>
 
  beforeEach(async () => {
    const scenario = await readScenario('./scenarios', 'user-api')
    handler = createReplayHandler(scenario)
  })
 
  test('creates new user', () => {
    const response = handler({
      method: 'POST',
      path: '/users',
      body: { name: 'New User' }
    })
 
    expect(response.status).toBe(201)
  })
})

Best Practices

1. Descriptive Scenario Names

// Good
createScenario('user-login-success', 'Successful user login with valid credentials')
createScenario('checkout-with-coupon', 'Complete checkout flow with discount coupon applied')
 
// Bad
createScenario('test1', 'test')
createScenario('scenario', 'stuff')

2. Include Relevant Headers

const recording = createRecording(
  { method: 'GET', path: '/users/1', timestamp: Date.now() },
  {
    status: 200,
    body: { id: 1, name: 'John' },
    headers: {
      'content-type': 'application/json',
      'cache-control': 'no-cache',
      'x-rate-limit-remaining': '99'
    },
    timestamp: Date.now()
  }
)

3. Use Path Parameters

// Good - uses parameter
path: '/users/:id'
 
// Avoid - hardcoded ID
path: '/users/123'

4. Test Error Cases

// Success case
const successRecording = createRecording(...)
 
// Not found
const notFoundRecording = createRecording(...)
 
// Bad request
const badRequestRecording = createRecording(...)
 
// Server error
const serverErrorRecording = createRecording(...)