. We wrap the native API with AbortController and exponential backoff for transient failures.
class DataPipeline {
private readonly endpoint: string;
private readonly timeoutMs: number;
private readonly maxRetries: number;
constructor(endpoint: string, timeoutMs = 5000, maxRetries = 2) {
this.endpoint = endpoint;
this.timeoutMs = timeoutMs;
this.maxRetries = maxRetries;
}
async execute(): Promise<CatalogEntry[]> {
let attempt = 0;
while (attempt <= this.maxRetries) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(this.endpoint, {
signal: controller.signal,
headers: { Accept: 'application/json' }
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const raw = await response.json();
return this.normalize(raw);
} catch (err) {
clearTimeout(timeoutId);
attempt++;
if (attempt > this.maxRetries) throw err;
await this.delay(attempt * 1000);
}
}
return [];
}
private normalize(raw: any[]): CatalogEntry[] {
return raw.map(item => ({
id: item.id,
title: item.title,
imageUrl: item.image,
unitPrice: parseFloat(item.price),
metrics: {
score: item.rating?.rate ?? 0,
count: item.rating?.count ?? 0
},
summary: item.description
}));
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Step 3: Batch DOM Construction
Sequential appendChild() calls force the browser to recalculate layout on every iteration. DocumentFragment batches mutations into a single reflow.
class CatalogRenderer {
private readonly container: HTMLElement;
constructor(selector: string) {
const el = document.querySelector(selector);
if (!(el instanceof HTMLElement)) throw new Error('Invalid container');
this.container = el;
}
render(entries: CatalogEntry[]): void {
this.container.innerHTML = '';
const fragment = document.createDocumentFragment();
entries.forEach(entry => {
fragment.appendChild(this.buildCard(entry));
});
this.container.appendChild(fragment);
}
private buildCard(entry: CatalogEntry): HTMLElement {
const card = document.createElement('article');
card.className = 'catalog-card';
card.setAttribute('role', 'listitem');
const title = document.createElement('h2');
title.textContent = entry.title;
const visual = document.createElement('img');
visual.src = entry.imageUrl;
visual.alt = `Visual representation of ${entry.title}`;
visual.loading = 'lazy';
visual.width = 280;
visual.height = 180;
const pricing = document.createElement('p');
pricing.className = 'price-tag';
pricing.textContent = `$${entry.unitPrice.toFixed(2)}`;
const rating = document.createElement('p');
rating.className = 'rating-badge';
rating.textContent = `β
${entry.metrics.score} (${entry.metrics.count})`;
const description = document.createElement('p');
description.className = 'summary-text';
description.textContent = entry.summary;
card.append(title, visual, pricing, rating, description);
return card;
}
}
Step 4: Orchestrate with State Management
Tie the pipeline and renderer together with explicit state transitions. This prevents race conditions and provides user feedback.
async function bootstrap(): Promise<void> {
const pipeline = new DataPipeline('https://fakestoreapi.com/products');
const renderer = new CatalogRenderer('#catalog-grid');
const statusEl = document.getElementById('render-status');
if (statusEl) statusEl.textContent = 'Fetching catalog...';
try {
const data = await pipeline.execute();
renderer.render(data);
if (statusEl) statusEl.textContent = `Loaded ${data.length} items`;
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown failure';
if (statusEl) statusEl.textContent = `Failed: ${message}`;
console.error('Catalog pipeline error:', err);
}
}
document.addEventListener('DOMContentLoaded', bootstrap);
Architecture Decisions & Rationale
- TypeScript over JavaScript: Enforces contract validation at compile time. API schema drift becomes a build-time error instead of a runtime crash.
AbortController + Timeout: Prevents zombie requests from consuming memory or blocking subsequent interactions. Critical for mobile networks.
DocumentFragment: Defers DOM insertion until the entire tree is assembled. Reduces layout thrashing by 80-90% compared to iterative appends.
- Explicit
width/height on <img>: Reserves layout space before network response, eliminating Cumulative Layout Shift (CLS) penalties.
textContent over innerHTML: Eliminates XSS attack surface. The source data is trusted, but production pipelines must enforce this boundary.
- Separation of Concerns:
DataPipeline handles network logic, CatalogRenderer handles DOM logic. Each can be unit-tested independently.
Pitfall Guide
1. Unhandled Promise Rejections
Explanation: Forgetting .catch() or wrapping await in try/catch leaves errors silent in production. Modern browsers log UnhandledPromiseRejection warnings, but users see blank UIs.
Fix: Always wrap async entry points in try/catch. Implement a global unhandledrejection listener as a safety net.
2. Sequential DOM Appends
Explanation: Calling appendChild() inside a loop forces synchronous style recalculation and layout on every iteration. The main thread blocks, causing jank.
Fix: Use DocumentFragment or innerHTML with pre-built strings. Batch mutations into a single frame.
3. Missing Loading & Error States
Explanation: Users perceive blank screens as broken applications. Without explicit state feedback, abandonment rates spike.
Fix: Implement a state machine (idle β loading β success/error). Render skeleton placeholders during fetch, and actionable error messages on failure.
4. Unbounded Image Loading
Explanation: Loading 50+ high-resolution images simultaneously saturates network queues and exhausts device memory.
Fix: Apply loading="lazy", explicit dimensions, and decoding="async". Consider intersection observers for progressive loading.
5. Ignoring Abort Signals
Explanation: Navigating away or triggering a new fetch while a previous request is pending creates race conditions and memory leaks.
Fix: Always pass AbortController.signal to fetch(). Cancel pending requests before initiating new ones.
6. Layout Shifts from Dynamic Content
Explanation: Images or text blocks that resize after rendering push surrounding elements, violating Core Web Vitals.
Fix: Reserve space with aspect-ratio, explicit dimensions, or skeleton loaders. Avoid height: auto on dynamic containers.
7. Blocking the Main Thread with Large Payloads
Explanation: Parsing and rendering 500+ items synchronously freezes UI interactions.
Fix: Chunk rendering with requestIdleCallback or setTimeout. Offload heavy parsing to Web Workers if payload exceeds 100 items.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Marketing landing page (<20 items) | Batched Vanilla TS | Zero dependencies, instant TTI, minimal maintenance | $0 hosting, low dev cost |
| Internal dashboard (50-200 items) | Framework (React/Vue) | Built-in state management, component reuse, dev tools | Moderate bundle cost, higher dev velocity |
| E-commerce catalog (500+ items) | SSR + Client Hydration | SEO, fast first paint, progressive enhancement | Higher infra cost, complex build pipeline |
| Real-time feed (WebSocket/SSE) | Hybrid (Framework + Stream) | Efficient diffing, connection management, UI reactivity | Moderate infra, high dev complexity |
Configuration Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catalog Interface</title>
<style>
:root {
--bg: #f8f9fa;
--card-bg: #ffffff;
--border: #e2e8f0;
--text-primary: #1a202c;
--text-secondary: #718096;
--accent: #3182ce;
}
body {
margin: 0;
padding: 2rem;
background: var(--bg);
font-family: system-ui, -apple-system, sans-serif;
color: var(--text-primary);
}
#render-status {
text-align: center;
margin-bottom: 1.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
#catalog-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.5rem;
}
.catalog-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.catalog-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.catalog-card h2 {
font-size: 1rem;
margin: 0.5rem 0;
text-align: center;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.catalog-card img {
width: 100%;
max-height: 160px;
object-fit: contain;
margin-bottom: 0.75rem;
}
.price-tag {
font-weight: 600;
color: var(--accent);
margin: 0.25rem 0;
}
.rating-badge {
font-size: 0.85rem;
color: var(--text-secondary);
margin: 0.25rem 0;
}
.summary-text {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: auto;
}
</style>
</head>
<body>
<p id="render-status">Initializing...</p>
<div id="catalog-grid" role="list"></div>
<script type="module" src="./catalog-pipeline.ts"></script>
</body>
</html>
Quick Start Guide
- Initialize TypeScript Project: Run
npm init -y && npm i typescript @types/node --save-dev && npx tsc --init. Enable strict: true and target: ES2020 in tsconfig.json.
- Paste Pipeline Code: Save the
DataPipeline, CatalogRenderer, and bootstrap logic into catalog-pipeline.ts. Ensure the HTML template references it via <script type="module">.
- Compile & Serve: Execute
npx tsc to generate JavaScript. Serve the directory locally using npx serve or any static server. Verify network tab shows single request with proper abort/timeout behavior.
- Validate Performance: Open DevTools β Performance tab. Record a load cycle. Confirm layout shifts are zero, main thread isn't blocked during render, and memory stabilizes after 500ms.