API Integration and Development Patterns
1. API Polyfills and Feature Detection Patterns
| Pattern | Description |
|---|---|
| Feature Detection First | Always check if API exists before using: if ("fetch" in window). |
| Conditional Polyfill Loading | Load polyfills only when needed to reduce bundle size. |
| Progressive Enhancement | Build base functionality, enhance for capable browsers. |
| Polyfill Service | Use CDN service (polyfill.io) for automatic polyfill delivery. |
| Core-js Integration | Use core-js for comprehensive ES6+ and Web API polyfills. |
| Babel Transformation | Transform modern syntax to compatible code at build time. |
Example: Comprehensive feature detection
// Feature detection utility
const FeatureDetector = {
// Check API support
supports(feature) {
const features = {
// Network APIs
fetch: "fetch" in window,
websocket: "WebSocket" in window,
// Storage APIs
localStorage: (() => {
try {
const test = "__test__";
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
})(),
indexedDB: "indexedDB" in window,
// Modern JavaScript
promise: "Promise" in window,
async: (() => {
try {
eval("(async () => {})");
return true;
} catch (e) {
return false;
}
})(),
modules: "noModule" in document.createElement("script"),
// DOM APIs
intersectionObserver: "IntersectionObserver" in window,
resizeObserver: "ResizeObserver" in window,
mutationObserver: "MutationObserver" in window,
// Service Worker
serviceWorker: "serviceWorker" in navigator,
// Media APIs
mediaRecorder: "MediaRecorder" in window,
webRTC: "RTCPeerConnection" in window,
// Graphics
canvas: (() => {
const canvas = document.createElement("canvas");
return !!(canvas.getContext && canvas.getContext("2d"));
})(),
webgl: (() => {
const canvas = document.createElement("canvas");
return !!(
canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl")
);
})(),
// Advanced features
webAssembly: typeof WebAssembly === "object",
webWorker: typeof Worker !== "undefined",
sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined"
};
return features[feature] !== undefined ? features[feature] : false;
},
// Get all unsupported features
getUnsupportedFeatures(requiredFeatures) {
return requiredFeatures.filter(feature => !this.supports(feature));
},
// Check browser capabilities
getBrowserCapabilities() {
return {
// Hardware
cores: navigator.hardwareConcurrency || 1,
memory: navigator.deviceMemory || "unknown",
// Network
online: navigator.onLine,
connection: navigator.connection?.effectiveType || "unknown",
// Screen
pixelRatio: window.devicePixelRatio || 1,
touch: "ontouchstart" in window || navigator.maxTouchPoints > 0
};
}
};
// Usage
const requiredFeatures = ["fetch", "promise", "localStorage"];
const unsupported = FeatureDetector.getUnsupportedFeatures(requiredFeatures);
if (unsupported.length > 0) {
console.warn("Missing features:", unsupported);
loadPolyfills(unsupported);
} else {
console.log("All features supported");
initApp();
}
Example: Conditional polyfill loading
// Dynamic polyfill loader
async function loadPolyfills() {
const polyfillsNeeded = [];
// Check and queue polyfills
if (!window.fetch) {
polyfillsNeeded.push(
import("whatwg-fetch")
);
}
if (!window.Promise) {
polyfillsNeeded.push(
import("promise-polyfill")
);
}
if (!window.IntersectionObserver) {
polyfillsNeeded.push(
import("intersection-observer")
);
}
if (!window.ResizeObserver) {
polyfillsNeeded.push(
import("resize-observer-polyfill")
);
}
if (!Element.prototype.closest) {
Element.prototype.closest = function(selector) {
let el = this;
while (el) {
if (el.matches(selector)) return el;
el = el.parentElement;
}
return null;
};
}
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
return this.indexOf(searchElement, fromIndex) !== -1;
};
}
// Load all polyfills in parallel
if (polyfillsNeeded.length > 0) {
console.log(`Loading ${polyfillsNeeded.length} polyfills...`);
await Promise.all(polyfillsNeeded);
console.log("Polyfills loaded");
}
}
// Initialize app after polyfills
loadPolyfills().then(() => {
console.log("Starting application");
initApp();
});
// Alternative: Use Polyfill.io CDN
/*
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,IntersectionObserver"></script>
*/
// Alternative: Conditional script loading
function loadPolyfillScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
async function loadLegacyPolyfills() {
if (!window.Promise) {
await loadPolyfillScript("https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js");
}
if (!window.fetch) {
await loadPolyfillScript("https://cdn.jsdelivr.net/npm/whatwg-fetch@3/dist/fetch.umd.js");
}
}
Note: Always use feature detection, never browser sniffing. Load polyfills conditionally to reduce bundle size. Use dynamic imports for code splitting. Consider using Polyfill.io CDN for automatic polyfill delivery based on user agent.
2. Error Handling and Graceful Degradation
| Strategy | Description |
|---|---|
| Try-Catch Blocks | Wrap API calls in try-catch for synchronous and async/await code. |
| Promise Error Handling | Use .catch() or try-catch with async/await for promise rejections. |
| Global Error Handler | Use window.onerror and window.onunhandledrejection for uncaught errors. |
| Fallback Strategies | Provide alternative implementations when APIs unavailable. |
| User Feedback | Show meaningful error messages to users, log details for developers. |
| Retry Logic | Implement exponential backoff for transient failures. |
Example: Comprehensive error handling
// Error handling utility
class ErrorHandler {
constructor() {
this.setupGlobalHandlers();
}
setupGlobalHandlers() {
// Handle uncaught errors
window.addEventListener("error", (event) => {
console.error("Uncaught error:", {
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno,
error: event.error
});
this.logError(event.error);
this.showUserError("An unexpected error occurred");
// Prevent default error handling
// event.preventDefault();
});
// Handle unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
this.logError(event.reason);
this.showUserError("An operation failed");
// Prevent default
// event.preventDefault();
});
}
// Wrap API calls with error handling
async safeApiCall(apiFunction, fallback = null) {
try {
return await apiFunction();
} catch (error) {
console.error("API call failed:", error);
this.logError(error);
if (fallback) {
console.log("Using fallback");
return fallback();
}
throw error;
}
}
// Retry with exponential backoff
async retry(fn, options = {}) {
const {
maxAttempts = 3,
initialDelay = 1000,
maxDelay = 10000,
backoffMultiplier = 2
} = options;
let lastError;
let delay = initialDelay;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxAttempts) {
console.error(`Failed after ${maxAttempts} attempts:`, error);
break;
}
console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await this.sleep(delay);
// Exponential backoff
delay = Math.min(delay * backoffMultiplier, maxDelay);
}
}
throw lastError;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
logError(error) {
// Send to error tracking service
if (window.errorTracker) {
window.errorTracker.log(error);
}
// Log to analytics
if (window.analytics) {
window.analytics.track("error", {
message: error.message,
stack: error.stack,
timestamp: Date.now()
});
}
}
showUserError(message) {
// Show user-friendly error message
const errorDiv = document.getElementById("error-message");
if (errorDiv) {
errorDiv.textContent = message;
errorDiv.style.display = "block";
setTimeout(() => {
errorDiv.style.display = "none";
}, 5000);
}
}
}
// Usage
const errorHandler = new ErrorHandler();
// Safe API call with fallback
async function loadData() {
return errorHandler.safeApiCall(
async () => {
const response = await fetch("/api/data");
if (!response.ok) throw new Error("Network error");
return response.json();
},
() => {
// Fallback: use cached data
return getCachedData();
}
);
}
// Retry failed operations
async function uploadFile(file) {
return errorHandler.retry(
async () => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/upload", {
method: "POST",
body: formData
});
if (!response.ok) throw new Error("Upload failed");
return response.json();
},
{ maxAttempts: 3, initialDelay: 1000 }
);
}
Example: API-specific graceful degradation
// Graceful degradation for various APIs
// 1. Fetch with XMLHttpRequest fallback
async function httpRequest(url, options = {}) {
if (window.fetch) {
try {
const response = await fetch(url, options);
return await response.json();
} catch (error) {
console.error("Fetch failed:", error);
throw error;
}
} else {
// Fallback to XMLHttpRequest
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(options.method || "GET", url);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("Network error"));
xhr.send(options.body);
});
}
}
// 2. IntersectionObserver with scroll fallback
function observeVisibility(element, callback) {
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
callback(entry.isIntersecting);
});
});
observer.observe(element);
return () => observer.disconnect();
} else {
// Fallback: use scroll events
const checkVisibility = () => {
const rect = element.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
callback(isVisible);
};
window.addEventListener("scroll", checkVisibility);
checkVisibility(); // Check initially
return () => window.removeEventListener("scroll", checkVisibility);
}
}
// 3. localStorage with cookie fallback
const storage = {
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
// Fallback to cookies
document.cookie = `${key}=${encodeURIComponent(JSON.stringify(value))}`;
}
},
get(key) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
// Fallback to cookies
const match = document.cookie.match(new RegExp(`${key}=([^;]+)`));
return match ? JSON.parse(decodeURIComponent(match[1])) : null;
}
}
};
// 4. WebSocket with long-polling fallback
class RealTimeConnection {
constructor(url) {
this.url = url;
this.useWebSocket = "WebSocket" in window;
}
connect(onMessage) {
if (this.useWebSocket) {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
onMessage(JSON.parse(event.data));
};
this.ws.onerror = () => {
console.warn("WebSocket failed, falling back to polling");
this.startPolling(onMessage);
};
} else {
this.startPolling(onMessage);
}
}
startPolling(onMessage) {
const poll = async () => {
try {
const response = await fetch(`${this.url}/poll`);
const data = await response.json();
onMessage(data);
} catch (error) {
console.error("Polling failed:", error);
}
setTimeout(poll, 2000); // Poll every 2 seconds
};
poll();
}
}
Note: Always handle errors at multiple levels: API level, function level, and global level. Provide fallbacks for missing APIs. Use retry logic for transient failures. Show user-friendly messages while logging detailed errors for debugging.
3. Performance Optimization for API Usage
| Technique | Description |
|---|---|
| Debouncing | Delay API calls until user stops action (e.g., typing). |
| Throttling | Limit API call frequency (e.g., max once per 100ms). |
| Request Caching | Cache API responses to avoid redundant requests. |
| Request Deduplication | Prevent duplicate simultaneous requests for same resource. |
| Lazy Loading | Load resources only when needed, not upfront. |
| Request Batching | Combine multiple API calls into single request. |
| Web Workers | Offload heavy processing to background thread. |
Example: Debouncing and throttling
// Debounce - wait for inactivity
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Throttle - limit frequency
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage: Debounced search
const searchAPI = debounce(async (query) => {
console.log("Searching for:", query);
const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
displayResults(results);
}, 300);
document.getElementById("search-input").addEventListener("input", (e) => {
searchAPI(e.target.value);
});
// Usage: Throttled scroll handler
const handleScroll = throttle(() => {
console.log("Scroll position:", window.scrollY);
updateScrollIndicator();
}, 100);
window.addEventListener("scroll", handleScroll);
Example: Request caching and deduplication
// Request cache with TTL
class RequestCache {
constructor(ttl = 5 * 60 * 1000) { // 5 minutes default
this.cache = new Map();
this.pendingRequests = new Map();
this.ttl = ttl;
}
async get(url, fetcher) {
// Check cache
const cached = this.cache.get(url);
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log("Cache hit:", url);
return cached.data;
}
// Deduplicate simultaneous requests
if (this.pendingRequests.has(url)) {
console.log("Request already pending:", url);
return this.pendingRequests.get(url);
}
// Make new request
console.log("Cache miss, fetching:", url);
const promise = fetcher().then(data => {
this.cache.set(url, {
data,
timestamp: Date.now()
});
this.pendingRequests.delete(url);
return data;
}).catch(error => {
this.pendingRequests.delete(url);
throw error;
});
this.pendingRequests.set(url, promise);
return promise;
}
clear() {
this.cache.clear();
this.pendingRequests.clear();
}
invalidate(url) {
this.cache.delete(url);
}
}
// Usage
const cache = new RequestCache();
async function getUserData(userId) {
return cache.get(`/api/users/${userId}`, async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
}
// Multiple calls will only make one request
Promise.all([
getUserData(1),
getUserData(1),
getUserData(1)
]).then(([user1, user2, user3]) => {
console.log("All resolved to same user:", user1 === user2);
});
Example: Request batching and Web Workers
// Request batching
class RequestBatcher {
constructor(batchInterval = 50) {
this.queue = [];
this.timer = null;
this.batchInterval = batchInterval;
}
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
if (!this.timer) {
this.timer = setTimeout(() => {
this.flush();
}, this.batchInterval);
}
});
}
async flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0);
this.timer = null;
try {
// Send batched request
const requests = batch.map(b => b.request);
const response = await fetch("/api/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ requests })
});
const results = await response.json();
// Resolve individual promises
batch.forEach((item, index) => {
item.resolve(results[index]);
});
} catch (error) {
// Reject all promises
batch.forEach(item => {
item.reject(error);
});
}
}
}
// Usage
const batcher = new RequestBatcher();
async function fetchUser(id) {
return batcher.add({ type: "user", id });
}
// These will be batched into single request
Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]).then(users => {
console.log("Users:", users);
});
// Web Worker for heavy processing
const workerCode = `
self.onmessage = function(event) {
const { data, operation } = event.data;
let result;
switch (operation) {
case "processImage":
result = processImage(data);
break;
case "parseData":
result = parseData(data);
break;
}
self.postMessage({ result });
};
function processImage(imageData) {
// Heavy image processing
return imageData;
}
function parseData(data) {
// Heavy data parsing
return data;
}
`;
// Create worker
const blob = new Blob([workerCode], { type: "application/javascript" });
const workerUrl = URL.createObjectURL(blob);
const worker = new Worker(workerUrl);
// Use worker
worker.postMessage({
operation: "processImage",
data: imageData
});
worker.onmessage = (event) => {
console.log("Worker result:", event.data.result);
};
Note: Performance optimization is critical for good UX. Debounce user input, throttle scroll/resize handlers. Cache API responses with appropriate TTL. Deduplicate simultaneous requests. Use Web Workers for CPU-intensive tasks to keep main thread responsive.
4. Cross-Browser Compatibility Strategies
| Strategy | Description |
|---|---|
| Feature Detection | Check API availability before use, provide fallbacks. |
| Vendor Prefixes | Try standard API first, then vendor-prefixed versions. |
| Autoprefixer | Use build tools to add vendor prefixes automatically. |
| Browserslist | Define target browsers for transpilation and polyfills. |
| Babel | Transpile modern JavaScript to compatible versions. |
| Can I Use | Check browser support data before using APIs. |
Example: Cross-browser API access
// Vendor prefix handling
function getVendorPrefixed(object, property) {
const prefixes = ["webkit", "moz", "ms", "o"];
// Try standard property first
if (property in object) {
return object[property];
}
// Try capitalized standard property
const capitalized = property.charAt(0).toUpperCase() + property.slice(1);
if (capitalized in object) {
return object[capitalized];
}
// Try vendor prefixes
for (const prefix of prefixes) {
const prefixedProperty = prefix + capitalized;
if (prefixedProperty in object) {
return object[prefixedProperty];
}
}
return null;
}
// Usage examples
// 1. RequestAnimationFrame
const requestAnimationFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return setTimeout(callback, 1000 / 60);
};
// 2. AudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = AudioContext ? new AudioContext() : null;
// 3. IndexedDB
const indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB;
// 4. FullScreen API
const fullscreenAPI = {
requestFullscreen: getVendorPrefixed(document.documentElement, "requestFullscreen"),
exitFullscreen: getVendorPrefixed(document, "exitFullscreen"),
fullscreenElement: () =>
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
};
async function enterFullscreen(element) {
const method =
element.requestFullscreen ||
element.webkitRequestFullscreen ||
element.mozRequestFullScreen ||
element.msRequestFullscreen;
if (method) {
await method.call(element);
}
}
// 5. Clipboard API
const clipboard = {
async writeText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
// Fallback using execCommand
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}
}
};
Example: Browser-specific workarounds
// Browser detection utilities (use sparingly!)
const BrowserDetect = {
isChrome() {
return /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
},
isFirefox() {
return /Firefox/.test(navigator.userAgent);
},
isSafari() {
return /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor);
},
isEdge() {
return /Edg/.test(navigator.userAgent);
},
isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
},
isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
};
// Safari-specific workaround for Date parsing
function parseDate(dateString) {
// Safari doesn't support YYYY-MM-DD HH:mm:ss format
if (BrowserDetect.isSafari()) {
// Convert to ISO format
dateString = dateString.replace(" ", "T");
}
return new Date(dateString);
}
// iOS Safari workaround for video autoplay
function setupVideoAutoplay(video) {
if (BrowserDetect.isIOS()) {
// iOS requires user interaction
video.muted = true;
video.playsInline = true;
// Try autoplay on user interaction
document.addEventListener("touchstart", () => {
video.play().catch(e => console.log("Autoplay prevented:", e));
}, { once: true });
} else {
video.autoplay = true;
}
}
// Cross-browser event normalization
function normalizeEvent(event) {
return {
target: event.target || event.srcElement,
which: event.which || event.keyCode,
preventDefault: () => {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
stopPropagation: () => {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
}
Note: Prefer feature detection over browser detection. Use vendor prefixes for CSS and JavaScript APIs when needed. Leverage build tools (Babel, Autoprefixer) for automatic compatibility. Define target browsers with Browserslist. Test on actual devices, not just browser DevTools.
5. API Mocking and Testing Patterns
| Pattern | Description |
|---|---|
| Mock Service Worker | Intercept network requests at Service Worker level for testing. |
| Jest Mocking | Mock APIs using Jest's mocking functions. |
| Stub Objects | Create fake API implementations for testing. |
| Dependency Injection | Pass API dependencies to make testing easier. |
| Test Doubles | Use spies, stubs, mocks, and fakes appropriately. |
| Fixture Data | Maintain realistic test data for API responses. |
Example: API mocking strategies
// 1. Simple fetch mock
const mockFetch = (response) => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(response)
})
);
};
// Usage in tests
test("fetches user data", async () => {
mockFetch({ id: 1, name: "John" });
const data = await fetchUser(1);
expect(data.name).toBe("John");
expect(fetch).toHaveBeenCalledWith("/api/users/1");
});
// 2. Mock Service Worker (MSW)
/*
import { rest } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
rest.get("/api/users/:id", (req, res, ctx) => {
const { id } = req.params;
return res(
ctx.json({ id, name: "John Doe" })
);
}),
rest.post("/api/users", (req, res, ctx) => {
return res(
ctx.status(201),
ctx.json({ id: 123, ...req.body })
);
})
);
// Enable mocking before all tests
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
*/
// 3. Dependency injection for testability
class UserService {
constructor(httpClient = fetch) {
this.httpClient = httpClient;
}
async getUser(id) {
const response = await this.httpClient(`/api/users/${id}`);
return response.json();
}
}
// Test with mock client
test("UserService.getUser", async () => {
const mockClient = jest.fn().mockResolvedValue({
json: () => Promise.resolve({ id: 1, name: "John" })
});
const service = new UserService(mockClient);
const user = await service.getUser(1);
expect(user.name).toBe("John");
expect(mockClient).toHaveBeenCalledWith("/api/users/1");
});
// 4. LocalStorage mock
const mockLocalStorage = (() => {
let store = {};
return {
getItem: jest.fn((key) => store[key] || null),
setItem: jest.fn((key, value) => {
store[key] = value.toString();
}),
removeItem: jest.fn((key) => {
delete store[key];
}),
clear: jest.fn(() => {
store = {};
})
};
})();
global.localStorage = mockLocalStorage;
// 5. IntersectionObserver mock
global.IntersectionObserver = class IntersectionObserver {
constructor(callback) {
this.callback = callback;
}
observe(element) {
// Simulate element being visible
this.callback([{
target: element,
isIntersecting: true,
intersectionRatio: 1
}]);
}
unobserve() {}
disconnect() {}
};
Example: Integration testing with real APIs
// Test helpers for API testing
class APITestHelpers {
constructor(baseURL) {
this.baseURL = baseURL;
}
async waitForCondition(conditionFn, timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await conditionFn()) {
return true;
}
await this.sleep(100);
}
throw new Error("Timeout waiting for condition");
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async setupTestData() {
// Create test data in database
await fetch(`${this.baseURL}/test/setup`, {
method: "POST"
});
}
async cleanupTestData() {
// Clean up test data
await fetch(`${this.baseURL}/test/cleanup`, {
method: "POST"
});
}
}
// Integration test example
describe("User API Integration", () => {
const helpers = new APITestHelpers("http://localhost:3000");
beforeEach(async () => {
await helpers.setupTestData();
});
afterEach(async () => {
await helpers.cleanupTestData();
});
test("creates and retrieves user", async () => {
// Create user
const createResponse = await fetch("http://localhost:3000/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Test User",
email: "test@example.com"
})
});
expect(createResponse.ok).toBe(true);
const createdUser = await createResponse.json();
expect(createdUser.id).toBeDefined();
// Retrieve user
const getResponse = await fetch(
`http://localhost:3000/api/users/${createdUser.id}`
);
expect(getResponse.ok).toBe(true);
const retrievedUser = await getResponse.json();
expect(retrievedUser.name).toBe("Test User");
});
test("handles network errors gracefully", async () => {
// Test with invalid URL
const response = await fetch("http://localhost:3000/api/invalid");
expect(response.ok).toBe(false);
expect(response.status).toBe(404);
});
});
Note: Mock APIs for unit tests, use real APIs for integration tests. Mock Service Worker provides realistic network mocking. Use dependency injection to make code testable. Mock browser APIs (localStorage, IntersectionObserver) in test environment.
6. Progressive Enhancement with Modern APIs
| Principle | Description |
|---|---|
| Core Functionality First | Ensure basic features work without modern APIs. |
| Enhancement Layers | Add features progressively based on capability detection. |
| Resilient Foundation | Build on HTML/CSS, enhance with JavaScript. |
| Feature Queries | Use CSS @supports and JavaScript feature detection. |
| Adaptive Loading | Adjust features based on device capabilities and network. |
| Graceful Fallbacks | Provide alternative experiences when APIs unavailable. |
Example: Progressive enhancement framework
// Progressive enhancement manager
class ProgressiveEnhancement {
constructor() {
this.features = this.detectFeatures();
this.applyEnhancements();
}
detectFeatures() {
return {
// Core APIs
fetch: "fetch" in window,
promises: "Promise" in window,
// Storage
localStorage: this.testLocalStorage(),
indexedDB: "indexedDB" in window,
// Observers
intersectionObserver: "IntersectionObserver" in window,
resizeObserver: "ResizeObserver" in window,
// Modern features
serviceWorker: "serviceWorker" in navigator,
webGL: this.testWebGL(),
webAssembly: typeof WebAssembly === "object",
// Device capabilities
touch: "ontouchstart" in window,
deviceMemory: navigator.deviceMemory,
connection: navigator.connection?.effectiveType
};
}
testLocalStorage() {
try {
const test = "__test__";
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
testWebGL() {
const canvas = document.createElement("canvas");
return !!(
canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl")
);
}
applyEnhancements() {
const html = document.documentElement;
// Add feature classes
Object.keys(this.features).forEach(feature => {
if (this.features[feature]) {
html.classList.add(`has-${feature}`);
} else {
html.classList.add(`no-${feature}`);
}
});
// Apply enhancements based on features
if (this.features.intersectionObserver) {
this.enableLazyLoading();
}
if (this.features.serviceWorker) {
this.registerServiceWorker();
}
if (this.features.webGL) {
this.enable3DGraphics();
}
// Adaptive loading based on device
if (this.features.connection === "slow-2g" || this.features.connection === "2g") {
this.enableDataSaver();
}
if (this.features.deviceMemory && this.features.deviceMemory < 4) {
this.reduceFeaturesForLowMemory();
}
}
enableLazyLoading() {
const images = document.querySelectorAll("img[data-src]");
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
images.forEach(img => observer.observe(img));
}
async registerServiceWorker() {
try {
await navigator.serviceWorker.register("/sw.js");
console.log("Service Worker registered");
} catch (error) {
console.error("SW registration failed:", error);
}
}
enable3DGraphics() {
console.log("3D graphics available");
// Initialize WebGL-based features
}
enableDataSaver() {
console.log("Data saver mode enabled");
// Reduce image quality, disable autoplay, etc.
}
reduceFeaturesForLowMemory() {
console.log("Reducing features for low memory device");
// Simplify UI, reduce animations, etc.
}
}
// Initialize on page load
document.addEventListener("DOMContentLoaded", () => {
new ProgressiveEnhancement();
});
Example: Adaptive component loading
// Adaptive component loader
class AdaptiveLoader {
constructor() {
this.capabilities = this.assessCapabilities();
}
assessCapabilities() {
return {
tier: this.getDeviceTier(),
network: this.getNetworkQuality(),
features: this.getSupportedFeatures()
};
}
getDeviceTier() {
const memory = navigator.deviceMemory || 4;
const cores = navigator.hardwareConcurrency || 2;
if (memory >= 8 && cores >= 8) return "high";
if (memory >= 4 && cores >= 4) return "medium";
return "low";
}
getNetworkQuality() {
const connection = navigator.connection;
if (!connection) return "unknown";
const effectiveType = connection.effectiveType;
if (effectiveType === "4g") return "fast";
if (effectiveType === "3g") return "medium";
return "slow";
}
getSupportedFeatures() {
return {
webGL: !!document.createElement("canvas").getContext("webgl"),
webAssembly: typeof WebAssembly === "object",
workers: typeof Worker !== "undefined"
};
}
async loadComponent(componentName) {
const { tier, network, features } = this.capabilities;
// Load appropriate version based on capabilities
if (tier === "high" && network === "fast") {
// Load full-featured version
return import(`./components/${componentName}/full.js`);
} else if (tier === "medium" || network === "medium") {
// Load standard version
return import(`./components/${componentName}/standard.js`);
} else {
// Load lite version
return import(`./components/${componentName}/lite.js`);
}
}
async loadMedia(mediaUrl) {
const { tier, network } = this.capabilities;
// Select appropriate media quality
let quality;
if (tier === "high" && network === "fast") {
quality = "high";
} else if (network === "slow" || tier === "low") {
quality = "low";
} else {
quality = "medium";
}
return `${mediaUrl}?quality=${quality}`;
}
}
// Usage
const loader = new AdaptiveLoader();
// Load component based on device capabilities
loader.loadComponent("VideoPlayer").then(({ default: VideoPlayer }) => {
const player = new VideoPlayer();
player.init();
});
// Load appropriate media quality
const videoUrl = await loader.loadMedia("/videos/intro.mp4");
videoElement.src = videoUrl;
Note: Progressive enhancement ensures everyone gets working experience, with better browsers getting enhanced features. Build resilient foundation with HTML/CSS. Enhance with JavaScript based on feature detection. Adapt to device capabilities and network conditions.
Warning: Don't assume all users have modern browsers or fast connections. Test on low-end devices and slow networks. Don't break core functionality for users without cutting-edge features. Monitor real-user metrics to understand actual device/network distribution.
API Integration and Development Best Practices
- Always use feature detection, never assume API availability
- Load polyfills conditionally to minimize bundle size
- Implement comprehensive error handling at all levels
- Provide meaningful fallbacks when APIs unavailable
- Debounce user input and throttle high-frequency events
- Cache API responses with appropriate TTL to reduce network calls
- Deduplicate simultaneous requests for same resource
- Use Web Workers for CPU-intensive operations
- Test across multiple browsers and devices
- Mock APIs for unit tests, use real APIs for integration tests
- Practice progressive enhancement - build on solid foundation
- Adapt features based on device capabilities and network quality
- Monitor performance and error rates in production