Request Interceptor Mode
The Request Interceptor Mode provides a lightweight alternative to mock servers. Instead of creating a mock server with fixture responses, it intercepts real HTTP requests to record interactions while allowing your application to call actual APIs.
When to Use Interceptor Mode
Section titled “When to Use Interceptor Mode”Interceptor Mode is perfect for:
- Integration testing against real APIs
- Development recording of actual API usage patterns
- Contract testing with live services
- Minimal setup scenarios where you don’t want to manage ports
Mock Server Mode is better for:
- Unit testing with isolated, deterministic responses
- Offline development when APIs aren’t available
- Fixture-based testing with approved test data
Mode Comparison
Section titled “Mode Comparison”| Feature | Mock Server Mode | Interceptor Mode |
|---|---|---|
| Purpose | Isolated testing with fixtures | Integration testing with real APIs |
| API Calls | Mocked responses | Real API responses |
| Port Required | Yes | No |
| Fixture Usage | Yes | No (just for operation matching) |
| Recording | Yes | Yes |
| Works with | Any HTTP client | fetch + http module clients |
| Cleanup | mock.close() | using statement |
Basic Usage
Section titled “Basic Usage”Automatic Cleanup with using
Section titled “Automatic Cleanup with using”The recommended approach uses the using statement for automatic cleanup:
import { createClient } from '@entente/consumer'
const entente = await createClient({ serviceUrl: 'https://entente.company.com', consumer: 'web-app', consumerVersion: '1.0.0', environment: 'test', recordingEnabled: true})
{ using interceptor = await entente.patchRequests('order-service', '2.1.0')
// All HTTP requests are now intercepted and recorded await fetch('https://api.example.com/orders/123') // Real API call await axios.get('https://api.example.com/orders') // Also intercepted
} // Automatically unpatch and batch send interactionsManual Cleanup
Section titled “Manual Cleanup”For more control over the lifecycle:
const interceptor = await entente.patchRequests('order-service', '2.1.0')
try { // Make API calls await fetch('https://api.example.com/orders/123')} finally { // Always clean up await interceptor.unpatch()}Testing Framework Integration
Section titled “Testing Framework Integration”Vitest/Jest with fetch
Section titled “Vitest/Jest with fetch”import { test, expect } from 'vitest'
test('should process order', async () => { using interceptor = await entente.patchRequests('order-service', '2.1.0')
// This calls real API and records interaction const response = await fetch('https://api.example.com/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: 'widget', quantity: 5 }) })
expect(response.status).toBe(201)
// Check intercepted calls const calls = interceptor.getInterceptedCalls() expect(calls).toHaveLength(1) expect(calls[0].operation).toBe('createOrder') // OpenAPI operation ID})Supertest (Express/Koa Apps)
Section titled “Supertest (Express/Koa Apps)”import request from 'supertest'import app from './app.js'
test('should get order', async () => { using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Supertest uses http module - also intercepted! const response = await request(app) .get('/orders/123') .expect(200)
const stats = interceptor.getStats() expect(stats.http).toBe(1) // HTTP module request intercepted expect(stats.total).toBe(1)})Axios and Other HTTP Clients
Section titled “Axios and Other HTTP Clients”import axios from 'axios'
test('should handle axios requests', async () => { using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Axios uses http module under the hood const response = await axios.get('https://api.example.com/orders/123')
expect(response.status).toBe(200)
const calls = interceptor.getInterceptedCalls() expect(calls[0].operation).toBe('getOrder') expect(calls[0].request.method).toBe('GET') expect(calls[0].response.status).toBe(200)})Configuration Options
Section titled “Configuration Options”Basic Options
Section titled “Basic Options”const interceptor = await entente.patchRequests('order-service', '2.1.0', { // Recording options recording: true, // Default: true
// Optional URL filter - only intercept specific URLs filter: (url) => url.includes('api.example.com')})URL Filtering
Section titled “URL Filtering”Filter which requests to intercept:
const interceptor = await entente.patchRequests('order-service', '2.1.0', { filter: (url) => { // Only intercept API calls return url.includes('/api/') || url.includes('api.example.com') }})
// These will be interceptedawait fetch('https://api.example.com/orders')await fetch('https://myapp.com/api/orders')
// These will NOT be interceptedawait fetch('https://analytics.example.com/track')await fetch('https://myapp.com/health')Operation Matching
Section titled “Operation Matching”The interceptor performs the same sophisticated operation matching as mock server mode:
OpenAPI Matching
Section titled “OpenAPI Matching”{ using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Matches OpenAPI operation by method + path pattern await fetch('https://api.example.com/orders/123') // → operation: 'getOrder'
await fetch('https://api.example.com/orders', { method: 'POST', ... }) // → operation: 'createOrder'}GraphQL Matching
Section titled “GraphQL Matching”{ using interceptor = await entente.patchRequests('user-service', '1.0.0')
// Parses GraphQL query to extract operation name await fetch('https://api.example.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: 'mutation CreateUser($input: UserInput!) { createUser(input: $input) { id } }' }) }) // → operation: 'createUser'
await fetch('https://api.example.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: 'query GetUser($id: ID!) { user(id: $id) { name email } }' }) }) // → operation: 'GetUser'}Inspection and Debugging
Section titled “Inspection and Debugging”View Intercepted Calls
Section titled “View Intercepted Calls”const interceptor = await entente.patchRequests('order-service', '2.1.0')
// Make some requestsawait fetch('https://api.example.com/orders/123')await fetch('https://api.example.com/orders', { method: 'POST', ... })
// Inspect what was capturedconst calls = interceptor.getInterceptedCalls()console.log('Intercepted calls:', calls.length)
calls.forEach(call => { console.log(`${call.request.method} ${call.request.url}`) console.log(`→ Operation: ${call.operation}`) console.log(`→ Status: ${call.response.status}`) console.log(`→ Duration: ${call.duration}ms`)})View Statistics
Section titled “View Statistics”const stats = interceptor.getStats()console.log(`Fetch requests: ${stats.fetch}`)console.log(`HTTP module requests: ${stats.http}`)console.log(`Total requests: ${stats.total}`)View Recorded Interactions
Section titled “View Recorded Interactions”// Interactions are recorded for Entente serviceconst interactions = interceptor.getRecordedInteractions()console.log(`Recorded ${interactions.length} interactions for upload`)Supported HTTP Clients
Section titled “Supported HTTP Clients”The interceptor works with any HTTP client built on standard Node.js APIs:
Fetch API
Section titled “Fetch API”- Native Node.js fetch (18+)
- fetch polyfills
- Direct fetch calls
HTTP Module Clients
Section titled “HTTP Module Clients”- axios - Popular HTTP client
- got - Lightweight HTTP client
- node-fetch - fetch for older Node versions
- supertest - Express/Koa testing
- request - Legacy HTTP client (deprecated)
- Custom clients built on
http/httpsmodules
Example with Multiple Clients
Section titled “Example with Multiple Clients”{ using interceptor = await entente.patchRequests('order-service', '2.1.0')
// All of these are intercepted await fetch('https://api.example.com/orders/1') await axios.get('https://api.example.com/orders/2') await got('https://api.example.com/orders/3')
const stats = interceptor.getStats() expect(stats.total).toBe(3)}Real-World Use Cases
Section titled “Real-World Use Cases”Integration Test Suite
Section titled “Integration Test Suite”describe('Order Processing Integration', () => { test('complete order workflow', async () => { using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Real API calls throughout the workflow const order = await createOrder({ product: 'widget', quantity: 5 }) await updateOrderStatus(order.id, 'processing') await addShippingInfo(order.id, { address: '123 Main St' }) const finalOrder = await getOrder(order.id)
expect(finalOrder.status).toBe('processing')
// Verify all operations were captured const calls = interceptor.getInterceptedCalls() expect(calls.map(c => c.operation)).toEqual([ 'createOrder', 'updateOrder', 'updateOrder', 'getOrder' ]) })})Development Recording
Section titled “Development Recording”// Record real usage patterns during developmentasync function recordDevelopmentSession() { using interceptor = await entente.patchRequests('order-service', '2.1.0', { recording: true, filter: (url) => url.includes('api.company.com') })
// Normal application flow await runApplication()
// All API interactions automatically recorded console.log(`Recorded ${interceptor.getInterceptedCalls().length} API calls`)}Contract Verification
Section titled “Contract Verification”test('verify compatibility with order-service v2.1.0', async () => { using interceptor = await entente.patchRequests('order-service', '2.1.0')
// Run your application against the real service await processOrderWorkflow()
// Verify all operations were successfully matched const calls = interceptor.getInterceptedCalls() const unknownOperations = calls.filter(c => c.operation === 'unknown')
expect(unknownOperations).toHaveLength(0)
// Verify expected operations were called const operations = calls.map(c => c.operation) expect(operations).toContain('createOrder') expect(operations).toContain('getOrder')})Manual Fixture Download
Section titled “Manual Fixture Download”For scenarios where you want fixtures but not full mock servers:
// Download approved fixtures for a service versionconst fixtures = await entente.downloadFixtures('order-service', '2.1.0')
console.log(`Downloaded ${fixtures.length} fixtures`)
// Use fixtures however you want:// - Save to disk for offline testing// - Use in custom mock implementations// - Analyze for test coverage
// Save to local fileimport { writeFileSync } from 'fs'writeFileSync( './fixtures/order-service-v2.1.0.json', JSON.stringify(fixtures, null, 2))Recording and Batching
Section titled “Recording and Batching”Automatic Recording
Section titled “Automatic Recording”In CI environments, interactions are automatically recorded and sent to Entente:
const entente = await createClient({ serviceUrl: 'https://entente.company.com', consumer: 'web-app', consumerVersion: '1.0.0', environment: 'test', recordingEnabled: process.env.CI === 'true' // Only record in CI})
{ using interceptor = await entente.patchRequests('order-service', '2.1.0')
// These interactions will be recorded in CI await processOrders()
} // Batch sent to Entente when disposedManual Recording Control
Section titled “Manual Recording Control”const interceptor = await entente.patchRequests('order-service', '2.1.0', { recording: false // Disable recording})
// Make requests without recordingawait fetch('https://api.example.com/orders/123')
// Enable recording for specific callsconst recordingInterceptor = await entente.patchRequests('order-service', '2.1.0', { recording: true})Implementation Details
Section titled “Implementation Details”Under the hood, the interceptor uses @mswjs/interceptors to patch:
- fetch API: Native fetch and fetch polyfills
- http module:
http.request,http.get,https.request,https.get - ClientRequest: Low-level HTTP client used by most libraries
This provides comprehensive coverage of HTTP clients in the Node.js ecosystem.
What’s Next?
Section titled “What’s Next?”- Mock Servers - Learn about the full mock server mode
- GitHub Actions - Integrate interceptor mode into CI/CD
- Fixtures - Understand fixture management and approval workflows
API Reference
Section titled “API Reference”RequestInterceptor Interface
Section titled “RequestInterceptor Interface”interface RequestInterceptor { // Control unpatch(): Promise<void> isPatched(): boolean
// Inspection getInterceptedCalls(): InterceptedCall[] getRecordedInteractions(): ClientInteraction[]
// Statistics getStats(): { fetch: number // Number of fetch requests intercepted http: number // Number of http module requests intercepted total: number // Total requests intercepted }
// Automatic cleanup [Symbol.dispose](): Promise<void>}InterceptedCall Structure
Section titled “InterceptedCall Structure”interface InterceptedCall { request: { method: string url: string headers: Record<string, string> body: unknown } response: { status: number headers: Record<string, string> body: unknown }
// Operation matching results operation: string // e.g., 'getOrder', 'createOrder' matchContext: { selectedOperationId: string candidates: Array<{ operationId: string confidence: number reasons: string[] }> }
// Metadata duration: number timestamp: Date consumer: string consumerVersion: string environment: string}