Working with APIs

Course 3 · Ch 5
Working with APIs: Pagination, Rate Limits, and Caching
The final chapter — combining everything from fetch through generators into the patterns real production API code actually uses

Fundamentals Chapter 10 covered a single fetch call in isolation. Real APIs rarely hand back everything in one response — they paginate, they rate-limit, and repeatedly re-fetching the same unchanged data wastes both time and the API's generosity. This final chapter brings together fetch, generators (Chapter 2), debounce (Chapter 4), and closures into the patterns that show up constantly in production code.

Pagination — Fetching Data in Pages

async function fetchPage(pageNumber) { const response = await fetch(`https://api.example.com/posts?page=${pageNumber}&limit=10`); return response.json(); // { data: [...10 items...], hasMore: true } } async function fetchAllPages() { let page = 1; let allPosts = []; let hasMore = true; while (hasMore) { const result = await fetchPage(page); allPosts = [...allPosts, ...result.data]; hasMore = result.hasMore; page++; } return allPosts; }

Most APIs return data in pages to limit response size — each call needs a page number, and a flag (here, hasMore) signals whether further pages exist. while (hasMore) keeps requesting and merging pages (using spread, Intermediate Chapter 1) until the API says there's nothing left.

A Generator Wrapping Pagination

async function* paginate(baseUrl) { let page = 1; let hasMore = true; while (hasMore) { const response = await fetch(`${baseUrl}?page=${page}`); const result = await response.json(); yield result.data; // hand back ONE page at a time, not all at once hasMore = result.hasMore; page++; } } for await (const pageOfPosts of paginate("https://api.example.com/posts")) { console.log("Got a page with", pageOfPosts.length, "items"); }

async function* combines Chapter 2's generators with async/await — each yield hands back one page as soon as it's fetched, rather than waiting for every page to load before returning anything. for await (... of ...) is for...of's counterpart for these async generators, awaiting each yielded value automatically.

Rate Limiting — Respecting an API's Request Caps

function createRateLimiter(maxPerSecond) { let queue = []; let processing = false; async function processQueue() { if (processing) return; processing = true; while (queue.length) { const { task, resolve } = queue.shift(); resolve(await task()); await new Promise(r => setTimeout(r, 1000 / maxPerSecond)); } processing = false; } return function limitedRequest(task) { return new Promise(resolve => { queue.push({ task, resolve }); processQueue(); }); }; } const limited = createRateLimiter(2); // max 2 requests per second for (let i = 1; i <= 5; i++) { limited(() => fetch(`https://api.example.com/item/${i}`)); }

Many APIs reject requests beyond a certain rate (e.g. "max 2 requests/second"). This rate limiter — using Chapter 4's module pattern, a queue, and a spaced-out setTimeout delay between each item — ensures requests are spread out automatically, regardless of how quickly the calling code tries to fire them off.

A 429 response means "slow down," not "broken"
HTTP status 429 Too Many Requests is the standard signal an API sends when its rate limit is exceeded. Production code should check for it specifically (response.status === 429) and back off — retrying immediately just gets rejected again, and repeatedly hitting it can lead to a temporary or permanent IP ban from the API provider.

Caching — Avoiding Repeated, Unnecessary Requests

function createCachedFetcher(ttlMs) { const cache = new Map(); return async function cachedFetch(url) { const cached = cache.get(url); if (cached && Date.now() - cached.timestamp < ttlMs) { return cached.data; // still fresh — skip the network entirely } const response = await fetch(url); const data = await response.json(); cache.set(url, { data, timestamp: Date.now() }); return data; }; } const fetchWithCache = createCachedFetcher(60000); // cache for 60 seconds

Another module-pattern closure, this time holding a Map (a key/value structure similar to Go's map from the Go course, distinct from a plain object) keyed by URL. ttlMs ("time to live") decides how long a cached response stays valid — repeated calls within that window return instantly from memory, with no network request at all.

ConcernTool/Pattern
Fetching multiple pageswhile loop checking a hasMore flag
Streaming pages one at a timeasync function* + for await...of
Respecting a rate limitA queue + spaced setTimeout delays between requests
Avoiding duplicate requestsA Map cache with a time-to-live check

Coding Challenges

Challenge 1

Write an async function fetchAllUsers() using the while-loop pagination pattern from this chapter, against https://jsonplaceholder.typicode.com/users (this particular API returns all results in one page, so simulate hasMore by stopping once the returned array's length is 0 on a fake "page 2"). Log the total count fetched.

📄 View solution
Challenge 2

Write the createCachedFetcher function from this chapter exactly as shown. Create fetchWithCache with a 5000ms TTL, call it twice in a row with the same URL (https://jsonplaceholder.typicode.com/posts/1), and log a message indicating whether each call hit the cache or made a real network request.

📄 View solution
Challenge 3

Write a function that checks a fetch Response object's status and, if it's 429, logs "Rate limited — backing off" and waits 2 seconds (using a Promise + setTimeout) before returning a retry signal; otherwise it returns the response's parsed JSON normally. Test it with a mocked response object of { status: 429 }.

📄 View solution

Chapter 5 Quick Reference

  • Pagination: loop while a hasMore-style flag is true, merging each page's results
  • async function* + for await...of — stream paginated results one page at a time
  • 429 Too Many Requests — the standard "you've hit the rate limit" HTTP status
  • Rate limiting: a queue + spaced delays, ensuring requests never exceed an allowed rate
  • Caching: a Map keyed by URL, with a timestamp + TTL check before reusing a cached value
  • All of these patterns combine closures, fetch, generators, and timers — no new core syntax
  • This completes JavaScript Advanced and the JavaScript course overall as currently planned across all 3 courses.