Skip to content

Request Matching

Return different responses for the same URL based on request content. Multiple mocks can exist for the same URL, and Scenarist selects the most specific match.

Use cases:

  • Different pricing for premium vs standard users
  • Different API responses based on version header
  • Different data based on query parameters
  • Tiered functionality based on request content

Use request matching when:

  • Same endpoint returns different responses based on who’s calling
  • You need to test different request payloads
  • API behavior varies by header values (API version, user tier, locale)
  • Query parameters change the response

Match on URL, request body, headers, or query parameters using the match field:

import type { ScenaristMock } from '@scenarist/express-adapter';
const mock: ScenaristMock = {
method: 'POST',
url: '/api/checkout',
match: {
url: /\/checkout$/, // URL pattern (native RegExp)
body: { tier: 'premium' }, // Partial body match
headers: { 'x-api-version': 'v2' }, // Exact header match
query: { detailed: 'true' }, // Exact query param match
},
response: { status: 200, body: { discount: 20 } }
};

Match URLs using strings, RegExp, or pattern strategies:

// Native RegExp (recommended)
match: {
url: /\/users\/\d+$/ // Match numeric user IDs only
}
// String strategies
match: {
url: { contains: '/api/v2/' } // URL contains substring
url: { startsWith: 'https://' } // URL starts with prefix
url: { endsWith: '/checkout' } // URL ends with suffix
}

This is useful when your mock’s url field uses path parameters but you need finer control:

{
method: 'GET',
url: 'https://api.github.com/users/:username', // Accepts any username
match: {
url: /\/users\/\d+$/ // But only match numeric usernames
},
response: { status: 200, body: { type: 'numeric-user' } }
}

Body matching is partial - only specified fields must match. The request can have additional fields:

match: {
body: { itemType: 'premium' } // Only checks itemType field
}
// Matches these requests:
// { itemType: 'premium', quantity: 5, color: 'red' } ✓
// { itemType: 'premium' } ✓
// { itemType: 'standard' } ✗

Header matching is exact for specified keys:

match: {
headers: {
'x-user-tier': 'premium',
'x-region': 'eu',
}
}
// Request must have these headers with exact values

Query parameter matching is exact for specified keys:

match: {
query: {
detailed: 'true',
units: 'metric',
}
}
// Request must have these query params with exact values

All criteria must match (AND logic):

match: {
body: { itemType: 'premium' },
headers: { 'x-user-tier': 'gold' },
query: { region: 'us' },
}
// All three must match for this mock to be selected

When multiple mocks match the same URL, Scenarist uses specificity scoring to choose the best match:

  • URL match = +1 point
  • Each body field = +1 point
  • Each header = +1 point
  • Each query param = +1 point
  • No match criteria = 0 points (fallback)

Most specific mock wins, regardless of order.

import type { ScenaristScenario } from '@scenarist/express-adapter';
const scenario: ScenaristScenario = {
id: 'tiered-pricing',
name: 'Tiered Pricing',
description: 'Different pricing based on specificity',
mocks: [
// Specificity: 2 (body.tier + body.category)
{
method: 'POST',
url: '/api/products',
match: {
body: { tier: 'premium', category: 'electronics' }
},
response: { status: 200, body: { discount: 30 } }
},
// Specificity: 1 (body.tier only)
{
method: 'POST',
url: '/api/products',
match: {
body: { tier: 'premium' }
},
response: { status: 200, body: { discount: 20 } }
},
// Specificity: 0 (no match criteria, fallback)
{
method: 'POST',
url: '/api/products',
response: { status: 200, body: { discount: 10 } }
}
]
};
// Request with tier='premium' and category='electronics'
// → Returns 30% discount (specificity 2 wins)
// Request with tier='premium' only
// → Returns 20% discount (specificity 1 wins)
// Request with neither
// → Returns 10% discount (fallback)

Since match criteria use AND logic, implement OR logic with separate mocks:

mocks: [
// Mock 1: Premium users
{
method: 'GET',
url: '/api/products',
match: { headers: { 'x-tier': 'premium' } },
response: { status: 200, body: { pricing: 'discounted' } }
},
// Mock 2: VIP users (OR - separate mock)
{
method: 'GET',
url: '/api/products',
match: { headers: { 'x-tier': 'vip' } },
response: { status: 200, body: { pricing: 'discounted' } }
},
// Fallback: Standard users
{
method: 'GET',
url: '/api/products',
response: { status: 200, body: { pricing: 'standard' } }
}
]

For OR logic within a single field, use regex in Pattern Matching →:

match: {
headers: {
'x-tier': { regex: { source: '^(premium|vip|enterprise)$', flags: '' } }
}
}
// Matches x-tier='premium' OR 'vip' OR 'enterprise' in one mock

When multiple mocks have equal specificity:

Mocks with match criteria (specificity > 0): First match wins

mocks: [
// Both have specificity: 1
{
match: { body: { type: 'premium' } },
response: { body: { discount: 20 } }, // ← Wins (first)
},
{
match: { body: { type: 'premium' } },
response: { body: { discount: 15 } },
},
]

Fallback mocks (specificity = 0): Last match wins

This enables active scenarios to override default scenario fallbacks:

// Default scenario fallback + active scenario fallback
mocks: [
{ response: { body: { tier: 'standard' } } }, // From default
{ response: { body: { tier: 'premium' } } }, // From active ← Wins (last)
]

When comparing fallback mocks (no match criteria), dynamic response types have higher priority than simple responses:

Mock TypeFallback Priority
sequence1 (higher)
stateResponse1 (higher)
response0 (lower)

This means a sequence or stateResponse from the default scenario cannot be overridden by a simple response from the active scenario.

// Default scenario has a sequence
const defaultScenario = {
mocks: [
{
method: 'GET',
url: '/api/job/status',
sequence: {
responses: [
{ status: 200, body: { status: 'pending' } },
{ status: 200, body: { status: 'complete' } }
],
repeat: 'last'
}
}
]
};
// Active scenario tries to override with simple response
const activeScenario = {
mocks: [
{
method: 'GET',
url: '/api/job/status',
response: { status: 200, body: { status: 'error' } } // ❌ Won't override!
}
]
};

Result: Default’s sequence (priority 1) wins over active’s response (priority 0).

To override a sequence or stateResponse, use one of these approaches:

Option 1: Use the same mock type

// ✅ sequence overrides sequence
mocks: [
{
method: 'GET',
url: '/api/job/status',
sequence: {
responses: [{ status: 200, body: { status: 'error' } }],
repeat: 'last'
}
}
]
// ✅ stateResponse overrides sequence
mocks: [
{
method: 'GET',
url: '/api/job/status',
stateResponse: {
default: { status: 200, body: { status: 'error' } }
}
}
]

Option 2: Add match criteria (priority 100+)

// ✅ response with match criteria overrides sequence fallback
mocks: [
{
method: 'GET',
url: '/api/job/status',
match: { headers: { 'x-force-error': 'true' } },
response: { status: 200, body: { status: 'error' } }
}
]
// Now send requests with x-force-error: true header
import type { ScenaristScenario } from '@scenarist/express-adapter';
export const tieredPricingScenario: ScenaristScenario = {
id: 'tiered-pricing',
name: 'Tiered Pricing',
description: 'Different pricing based on user tier and item type',
mocks: [
// Premium users buying premium items - best discount
{
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
match: {
body: { itemType: 'premium' },
headers: { 'x-user-tier': 'gold' },
},
response: {
status: 200,
body: { amount: 7000, discount: 'gold_premium_30' },
},
},
// Premium items (any user)
{
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
match: { body: { itemType: 'premium' } },
response: {
status: 200,
body: { amount: 8000, discount: 'premium_20' },
},
},
// Standard items
{
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
match: { body: { itemType: 'standard' } },
response: {
status: 200,
body: { amount: 10000 },
},
},
// Fallback for other item types
{
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
response: {
status: 200,
body: { amount: 5000 },
},
},
],
};