Data Fetching and APIs
Quick reference for API integration and data fetching in LARC applications. For detailed tutorials, see Learning LARC Chapter 11.
Overview
Modern web applications fetch data from APIs using REST, GraphQL, or WebSockets. LARC provides components and patterns for handling async data loading, caching, error recovery, and real-time updates.
Key Concepts:- REST APIs: Standard HTTP methods (GET, POST, PUT, DELETE)
- GraphQL: Query-based data fetching with precise field selection
- Error handling: Retry logic, fallbacks, user feedback
- Caching strategies: In-memory, localStorage, IndexedDB
- Loading states: Skeleton screens, spinners, optimistic updates
Quick Example
class ProductList extends LarcComponent {
constructor() {
super();
this.products = [];
this.loading = true;
this.error = null;
}
async connectedCallback() {
await this.loadProducts();
}
async loadProducts() {
this.loading = true;
this.error = null;
this.render();
try {
const response = await fetch('/api/products');
if (!response.ok) throw new Error('Failed to load products');
this.products = await response.json();
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
this.render();
}
}
template() {
if (this.loading) return '<div class="spinner">Loading...</div>';
if (this.error) return `<div class="error">${this.error}</div>`;
return `
<div class="product-list">
${this.products.map(p => `
<div class="product-card">
<h3>${p.name}</h3>
<p>${p.price}</p>
</div>
`).join('')}
</div>
`;
}
}
REST API Patterns
| HTTP Method | Purpose | Example |
|-------------|---------|---------|
| GET | Fetch data | GET /api/products?category=electronics |
| POST | Create resource | POST /api/products + body |
| PUT | Replace resource | PUT /api/products/123 + body |
| PATCH | Update fields | PATCH /api/products/123 + partial body |
| DELETE | Remove resource | DELETE /api/products/123 |
REST Example with Error Handling
async createProduct(data) {
try {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create product');
}
return await response.json();
} catch (err) {
console.error('Create product failed:', err);
throw err;
}
}
GraphQL Integration
Basic Query
async fetchUser(userId) {
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`;
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables: { id: userId }
})
});
const result = await response.json();
if (result.errors) throw new Error(result.errors[0].message);
return result.data.user;
}
Mutation Example
async updateUser(userId, updates) {
const mutation = `
mutation UpdateUser($id: ID!, $input: UserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
`;
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: mutation,
variables: { id: userId, input: updates }
})
});
const result = await response.json();
if (result.errors) throw new Error(result.errors[0].message);
return result.data.updateUser;
}
Caching Strategies
| Strategy | Implementation | Use Case |
|----------|---------------|----------|
| In-memory | Store in component property | Session-only data |
| localStorage | localStorage.setItem() | Small datasets, settings |
| IndexedDB | pan-idb component | Large datasets, offline support |
| HTTP cache | Cache-Control headers | Static assets, CDN content |
| Stale-while-revalidate | Show cached + fetch fresh | Balance speed + freshness |
Stale-While-Revalidate Pattern
async loadProducts() {
// Show cached data immediately
const cached = this.getCache('products');
if (cached) {
this.products = cached;
this.render();
}
// Fetch fresh data in background
try {
const response = await fetch('/api/products');
const fresh = await response.json();
this.products = fresh;
this.setCache('products', fresh, 5 * 60 * 1000); // 5 min TTL
this.render();
} catch (err) {
// Keep showing cached data on error
if (!cached) {
this.error = err.message;
this.render();
}
}
}
getCache(key) {
const item = localStorage.getItem(`cache:${key}`);
if (!item) return null;
const { data, expires } = JSON.parse(item);
if (Date.now() > expires) return null;
return data;
}
setCache(key, data, ttl) {
localStorage.setItem(`cache:${key}`, JSON.stringify({
data,
expires: Date.now() + ttl
}));
}
Error Recovery Patterns
Retry with Exponential Backoff
async fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) return await response.json();
if (response.status >= 500 && i < maxRetries - 1) {
// Retry on server errors
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw new Error(`HTTP ${response.status}`);
} catch (err) {
if (i === maxRetries - 1) throw err;
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Fallback Data
async loadProducts() {
try {
const response = await fetch('/api/products');
this.products = await response.json();
} catch (err) {
console.error('API failed, using fallback data:', err);
this.products = this.getFallbackProducts();
}
this.render();
}
getFallbackProducts() {
return [
{ id: 1, name: 'Product 1', price: 19.99 },
{ id: 2, name: 'Product 2', price: 29.99 }
];
}
Loading States
Skeleton Screens
template() {
if (this.loading) {
return `
<div class="product-list">
${Array(6).fill(0).map(() => `
<div class="product-card skeleton">
<div class="skeleton-title"></div>
<div class="skeleton-text"></div>
<div class="skeleton-price"></div>
</div>
`).join('')}
</div>
`;
}
return this.renderProducts();
}
Progressive Loading
async connectedCallback() {
// Load critical data first
await this.loadSummary();
this.render();
// Load details in background
await this.loadDetails();
this.render();
}
Component Reference
- pan-data-connector: REST API integration - See Chapter 20
- pan-graphql-connector: GraphQL integration - See Chapter 20
- pan-websocket: WebSocket connection management - See Chapter 20
- pan-idb: IndexedDB caching - See Chapter 18
Complete Example: Product Search with Caching
class ProductSearch extends LarcComponent {
constructor() {
super();
this.products = [];
this.loading = false;
this.error = null;
this.searchTerm = '';
this.debounceTimer = null;
}
async connectedCallback() {
// Load cached results
const cached = this.getCache('products-all');
if (cached) {
this.products = cached;
this.render();
}
// Fetch fresh data
await this.loadProducts();
}
async loadProducts(search = '') {
this.loading = true;
this.error = null;
this.render();
try {
const url = search
? `/api/products/search?q=${encodeURIComponent(search)}`
: '/api/products';
const response = await fetch(url);
if (!response.ok) throw new Error('Search failed');
this.products = await response.json();
// Cache full product list only
if (!search) {
this.setCache('products-all', this.products, 5 * 60 * 1000);
}
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
this.render();
}
}
handleSearchInput(event) {
this.searchTerm = event.target.value;
// Debounce search
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.loadProducts(this.searchTerm);
}, 300);
}
getCache(key) {
const item = localStorage.getItem(`cache:${key}`);
if (!item) return null;
const { data, expires } = JSON.parse(item);
return Date.now() < expires ? data : null;
}
setCache(key, data, ttl) {
localStorage.setItem(`cache:${key}`, JSON.stringify({
data,
expires: Date.now() + ttl
}));
}
template() {
return `
<div class="product-search">
<input
type="search"
placeholder="Search products..."
value="${this.searchTerm}"
oninput="this.handleSearchInput(event)">
${this.loading ? '<div class="spinner">Searching...</div>' : ''}
${this.error ? `<div class="error">${this.error}</div>` : ''}
<div class="product-grid">
${this.products.map(product => `
<div class="product-card">
<img src="${product.image}" alt="${product.name}">
<h3>${product.name}</h3>
<p class="price">$${product.price}</p>
<button onclick="addToCart(${product.id})">Add to Cart</button>
</div>
`).join('')}
</div>
${this.products.length === 0 && !this.loading ? '
<div class="no-results">No products found</div>
' : ''}
</div>
`;
}
}
customElements.define('product-search', ProductSearch);
Cross-References
- Tutorial: Learning LARC Chapter 11 (Data Fetching and APIs)
- Components: Chapter 20 (pan-data-connector, pan-graphql-connector, pan-websocket)
- Patterns: Appendix E (API Integration Patterns)
- Related: Chapter 4 (State Management), Chapter 9 (Realtime Features)
Common Issues
Issue: CORS errors in development
Problem:Access-Control-Allow-Origin errors
Solution: Configure dev server proxy or add CORS headers to API
Issue: Stale cache data
Problem: Users see outdated information Solution: Implement cache invalidation with TTL, version keys, or manual clearIssue: Request waterfall
Problem: Sequential requests slow down page load Solution: Batch requests, use GraphQL, or fetch data in parallelIssue: Memory leaks from pending requests
Problem: Component unmounts but fetch continues Solution: Use AbortController to cancel requests indisconnectedCallback()
Issue: Authentication tokens expired
Problem: 401 errors on API calls Solution: Implement token refresh interceptor before retrying failed requestsSee Learning LARC Chapter 11 for detailed API patterns, authentication flows, and advanced caching strategies.