@jarrodek/amw
v0.1.1
Published
A library to mock API calls in a web browser using service workers
Downloads
263
Maintainers
Readme
AMW - API Mocking for Web
A modern TypeScript library for mocking API calls in web browsers using Service Workers. AMW provides a non-invasive way to intercept and mock HTTP requests with full access to test context, closures, and dynamic response generation.
Features
- 🚀 Service Worker-based: Non-invasive network interception (no
fetchpatching) - 🔄 Distributed Registry: Fast routing in SW, flexible execution on Main Thread
- 🎯 Dynamic Responses: Status, headers, and body can be static or async functions
- 🧪 Closure Access: Response generators access test variables and closures
- 🎯 Modern APIs: Uses URLPattern, MessageChannel, and ES2022+ features
- 📦 Zero Dependencies: Built entirely on native Web APIs
- 🔧 Extensible: Class-based SW design allows custom implementations
- 💪 TypeScript: Full type safety and IDE support
- ✅ Well-tested: 198 tests with 90% code coverage
- ⚡ Evergreen Browsers: Targets modern browsers only, no polyfills
Installation
npm install @jarrodek/amwQuick Start
1. Copy the Service Worker
Copy the default Service Worker script to your public directory:
cp node_modules/@jarrodek/amw/dist/sw.js public/amw-sw.js2. Setup in Your Tests
import { setupWorker, type MockHandler } from '@jarrodek/amw';
let mock: MockHandler;
beforeAll(async () => {
mock = await setupWorker({
swPath: '/amw-sw.js',
base: 'https://api.example.com/v1'
});
});
afterAll(async () => {
await mock.stop();
});
afterEach(() => {
mock.reset(); // Clear mocks between tests
});3. Add Mock Handlers
it('mocks a GET request', async () => {
await mock.add({
match: { uri: '/users/:id' },
respond: {
status: 200,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id: '123', name: 'John Doe' })
}
});
const res = await fetch('https://api.example.com/v1/users/123');
const data = await res.json();
expect(data.name).toBe('John Doe');
});Advanced Usage
Dynamic Responses
All response properties (status, headers, body) can be static values or async functions:
it('generates dynamic responses', async () => {
const localId = '456';
const localName = 'Jane Smith';
await mock.add({
match: { uri: '/users/:id' },
respond: {
status: (req) => req.params.id === '999' ? 404 : 200,
headers: (req) => ({
'content-type': 'application/json',
'x-user-id': req.params.id,
}),
body: async (req) => {
// Full access to test scope!
return JSON.stringify({
id: localId,
name: localName,
requestedId: req.params.id
});
}
}
});
const res = await fetch('https://api.example.com/v1/users/999');
expect(res.status).toBe(404);
const data = await res.json();
expect(data.id).toBe('456'); // Uses closure variable
expect(data.requestedId).toBe('999'); // From URL params
});Request Matching
Match requests by URI pattern, HTTP methods, and required headers:
await mock.add({
match: {
uri: '/api/data',
methods: ['POST', 'PUT'],
headers: {
'authorization': 'Bearer token123',
'content-type': 'application/json'
}
},
respond: {
status: 201,
body: 'Created'
}
});URL Pattern Syntax:
/users/:id- Named parameter/api/*- Wildcard/files/:name.:ext- Multiple parameters- Uses native URLPattern API
Transient Mocks (Lifetime)
Limit how many times a handler can be used:
// This mock expires after 2 uses
await mock.add({
match: { uri: '/one-time' },
respond: { body: 'First call' }
}, { lifetime: 2 });
await fetch('/one-time'); // Mocked
await fetch('/one-time'); // Mocked
await fetch('/one-time'); // Goes to networkNetwork Error Simulation
Simulate various network failure scenarios:
// Network error
await mock.add({
match: { uri: '/error' },
respond: {
error: 'network' // or 'timeout', 'offline'
}
});
await fetch('/error'); // Throws TypeError: Failed to fetchBinary Data Support
Return binary data as ArrayBuffer:
await mock.add({
match: { uri: '/download/:filename' },
respond: {
status: 200,
headers: { 'content-type': 'application/pdf' },
body: async (req) => {
// Return ArrayBuffer for binary data
const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46]); // %PDF
return pdfData.buffer;
}
}
});
const res = await fetch('/download/document.pdf');
const buffer = await res.arrayBuffer();
const bytes = new Uint8Array(buffer);
console.log(bytes[0]); // 0x25 ('%')Custom Service Worker
For advanced use cases, extend the MockServiceWorker class:
// custom-sw.ts
import { MockServiceWorker } from '@jarrodek/amw/worker';
class LoggingWorker extends MockServiceWorker {
protected onFetch(event: FetchEvent) {
console.log('[Custom SW] Intercepting:', event.request.url);
super.onFetch(event);
}
}
const worker = new LoggingWorker();
worker.start();Build and use your custom worker:
const mock = await setupWorker({
swPath: '/custom-sw.js'
});API Reference
setupWorker(options?)
Sets up the Service Worker and returns a MockHandler instance.
Options:
swPath?: string- Path to SW script (default:/amw-sw.js)scope?: string- SW registration scope (default:/)base?: string- Base URL for resolving relative URIs
Returns: Promise<MockHandler>
MockHandler
add(handler, options?)
Registers a mock interceptor.
Handler:
match: InterceptMatcher- Matching criteriauri: string- URLPattern string (e.g.,/users/:id)methods?: string[]- HTTP methods to match (default: all)headers?: Record<string, string>- Required request headers
respond: ResponseGenerator- Response generation logicstatus?: number | (req) => number | Promise<number>- HTTP status (default: 200)headers?: Record<string, string> | (req) => Record<string, string> | Promise<Record<string, string>>- Response headersbody?: string | ArrayBuffer | null | (req) => string | ArrayBuffer | null | Promise<...>- Response bodyerror?: 'network' | 'timeout' | 'offline'- Simulate network error
Options:
lifetime?: number- Max usage count (default:Infinity)strategy?: 'mock' | 'passthrough'- Handling strategy (default:'mock')
Returns: Promise<void>
release(uri: string)
Removes all handlers for a specific URI.
Returns: Promise<void>
releaseMatch(matcher: InterceptMatcher)
Removes handlers matching specific criteria.
Returns: Promise<void>
reset()
Removes all handlers.
Returns: Promise<void>
stop()
Unregisters the Service Worker and cleans up.
Returns: Promise<void>
Package Exports
AMW provides multiple entry points for different use cases:
// Main API (for tests)
import { setupWorker, type MockHandler } from '@jarrodek/amw';
// Service Worker exports (for custom SW)
import { MockServiceWorker } from '@jarrodek/amw/worker';
// Pre-built Service Worker (copy to public directory)
import '@jarrodek/amw/sw';Architecture
AMW uses a Distributed Registry model:
- Service Worker (Router): Holds URL patterns and matching logic
- Main Thread (Executor): Holds response generators with closure access
- MessageChannel: Dedicated communication for each request
This design ensures:
- ⚡ Fast routing (native URLPattern in SW)
- 🎯 Closure access (generators run on Main Thread)
- 🔒 Isolated communication (MessagePort per request)
- 🚀 No fetch patching or monkey-patching
- ✅ Clean separation of concerns
Request Flow
- Browser makes a
fetch()request - Service Worker intercepts via
FetchEvent - SW matches request against stored URLPatterns
- SW sends request details to Main Thread via MessagePort
- Main Thread executes response generator with test context
- Main Thread sends response data back to SW
- SW constructs and returns
Responseobject
Browser Support
AMW requires modern evergreen browsers with:
- Service Workers
- URLPattern API
- MessageChannel
- ES2022+ features
Tested on:
- ✅ Chromium 95+ (Chrome, Edge, Opera)
- ✅ Firefox 106+
- ✅ WebKit/Safari 16.4+
Note: Some advanced features may have browser-specific limitations. For example, WebKit/Safari has known issues with Service Worker redirect responses.
Testing
The library includes a comprehensive test suite:
npm test # Run all tests
npm run test:chrome # Chromium only
npm run test:firefox # Firefox only
npm run test:webkit # WebKit onlyTest Coverage:
- 198 tests across 13 test suites
- ~90% code coverage
- Tests for setup, matching, dynamic responses, error handling, edge cases, performance, real-world patterns, and browser-specific HTTP features
License
Apache-2.0
Author
Pawel Uchida-Psztyc
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
For development:
npm install # Install dependencies
npm run build # Build TypeScript and bundle SW
npm test # Run tests
npm run format # Format code with ESLint & PrettierChangelog
See GitHub Releases for version history and changes.
