TypeFetched/manual/chapter-3-get-requests.md
Casey Collier b85b9a63e2 Initial commit: TypedFetch - Zero-dependency, type-safe HTTP client
Features:
- Zero configuration, just works out of the box
- Runtime type inference and validation
- Built-in caching with W-TinyLFU algorithm
- Automatic retries with exponential backoff
- Circuit breaker for resilience
- Request deduplication
- Offline support with queue
- OpenAPI schema discovery
- Full TypeScript support with type descriptors
- Modular architecture
- Configurable for advanced use cases

Built with bun, ready for npm publishing
2025-07-20 12:35:43 -04:00

17 KiB
Raw Blame History

Chapter 3: The Magic of GET Requests

"Reading is fundamental - especially when reading data from APIs."


The Read-Only Superpower

Sarah had been using TypedFetch for a week now, and her Weather Buddy app was getting popular at the office. But her colleague Marcus had a challenge: "Can you make it show weather for multiple cities at once? And add search suggestions as I type?"

"That's going to need a lot of GET requests," Sarah said.

Marcus grinned. "Good thing GET requests are TypedFetch's specialty."

GET Requests: The Workhorses of the Web

If APIs were a library, GET requests would be checking out books. You're not changing anything - just reading information. And it turns out, 80% of API calls you'll ever make are GET requests.

With TypedFetch, GET requests aren't just simple - they're powerful. Let's explore.

Query Parameters: Asking Specific Questions

Remember our restaurant metaphor? Query parameters are like asking your waiter for modifications: "Can I get the burger without pickles? Extra fries? Medium-rare?"

The Manual Way (Ugh):

// Building URLs by hand is error-prone
const city = 'San Francisco'
const units = 'metric'
const url = `https://api.weather.com/data?city=${encodeURIComponent(city)}&units=${units}`

The TypedFetch Way:

const { data } = await tf.get('https://api.weather.com/data', {
  params: {
    city: 'San Francisco',  // Automatically encoded!
    units: 'metric'
  }
})

TypedFetch handles all the encoding for you. Spaces, special characters, Unicode - all taken care of.

Let's build a city search with auto-complete:

async function searchCities(query) {
  const { data } = await tf.get('https://api.teleport.org/api/cities/', {
    params: {
      search: query,
      limit: 5
    }
  })
  
  return data._embedded['city:search-results'].map(city => ({
    name: city.matching_full_name,
    population: city.population,
    country: city._links['city:country'].name
  }))
}

// Usage
const cities = await searchCities('New')
// Returns: New York, New Orleans, New Delhi, etc.

Headers: Your API Passport

Headers are like showing your ID at a club. They tell the API who you are and what you want.

// Common headers you'll need
const { data } = await tf.get('https://api.github.com/user/repos', {
  headers: {
    'Authorization': 'Bearer ghp_yourtoken123',  // Authentication
    'Accept': 'application/vnd.github.v3+json',   // API version
    'X-GitHub-Api-Version': '2022-11-28'          // Specific version
  }
})

Pro Tip: Setting Default Headers

If you're always sending the same headers, set them once:

// Create a custom instance
import { createTypedFetch } from 'typedfetch'

const github = createTypedFetch()

// Add auth to every request
github.addRequestInterceptor(config => ({
  ...config,
  headers: {
    ...config.headers,
    'Authorization': 'Bearer ghp_yourtoken123'
  }
}))

// Now all requests include auth
const { data: repos } = await github.get('https://api.github.com/user/repos')
const { data: gists } = await github.get('https://api.github.com/gists')

Pagination: Getting Data in Chunks

Most APIs don't dump thousands of records on you at once. They paginate - giving you data in bite-sized chunks.

async function getAllUsers() {
  const users = []
  let page = 1
  let hasMore = true
  
  while (hasMore) {
    const { data } = await tf.get('https://api.example.com/users', {
      params: { page, limit: 100 }
    })
    
    users.push(...data.users)
    hasMore = data.hasNextPage
    page++
  }
  
  return users
}

Smarter Pagination with Generators

For large datasets, loading everything into memory isn't smart. Use generators:

async function* paginatedUsers() {
  let page = 1
  let hasMore = true
  
  while (hasMore) {
    const { data } = await tf.get('https://api.example.com/users', {
      params: { page, limit: 100 }
    })
    
    // Yield each user one at a time
    for (const user of data.users) {
      yield user
    }
    
    hasMore = data.hasNextPage
    page++
  }
}

// Process users without loading all into memory
for await (const user of paginatedUsers()) {
  console.log(user.name)
  // Process one user at a time
}

Real-Time Updates: Polling Done Right

Want live data? The simplest approach is polling - repeatedly checking for updates:

function pollWeather(city, callback, interval = 60000) {
  // Immediately fetch
  updateWeather()
  
  // Then poll every interval
  const timer = setInterval(updateWeather, interval)
  
  async function updateWeather() {
    try {
      const { data } = await tf.get('https://wttr.in/v2', {
        params: { 
          location: city,
          format: 'j1'
        }
      })
      callback(data)
    } catch (error) {
      console.error('Weather update failed:', error.message)
      // Don't stop polling on error
    }
  }
  
  // Return cleanup function
  return () => clearInterval(timer)
}

// Usage
const stopPolling = pollWeather('Tokyo', weather => {
  console.log(`Tokyo is ${weather.current_condition[0].temp_C}°C`)
})

// Stop when done
// stopPolling()

Performance Tricks: Making GET Requests Fly

1. Parallel Requests

When you need data from multiple endpoints, don't wait:

// ❌ Slow - Sequential
const user = await tf.get('/api/user/123')
const posts = await tf.get('/api/user/123/posts')  
const comments = await tf.get('/api/user/123/comments')

// ✅ Fast - Parallel
const [user, posts, comments] = await Promise.all([
  tf.get('/api/user/123'),
  tf.get('/api/user/123/posts'),
  tf.get('/api/user/123/comments')
])

2. Conditional Requests

Only fetch if data changed:

// Using ETags
const { data, response } = await tf.get('/api/resource')
const etag = response.headers.get('etag')

// Later, only get if changed
const { data: newData, response: newResponse } = await tf.get('/api/resource', {
  headers: {
    'If-None-Match': etag
  }
})

if (newResponse.status === 304) {
  console.log('Data hasn't changed!')
}

3. Selective Fields

Many APIs let you choose what data to return:

// Get only what you need
const { data } = await tf.get('https://api.github.com/users/torvalds', {
  params: {
    fields: 'login,name,avatar_url,public_repos'
  }
})

Let's Build: Weather Buddy 3.0 - Multi-City Dashboard

Time to put it all together:

<!DOCTYPE html>
<html>
<head>
    <title>Weather Buddy 3.0 - Multi-City Dashboard</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .city-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
        .city-card { border: 1px solid #ddd; padding: 15px; border-radius: 8px; }
        .search-box { margin-bottom: 20px; }
        .search-suggestions { border: 1px solid #ddd; max-height: 200px; overflow-y: auto; }
        .suggestion { padding: 10px; cursor: pointer; }
        .suggestion:hover { background: #f0f0f0; }
        .loading { color: #666; }
        .error { color: red; }
    </style>
    <script type="module">
        import { tf } from 'https://esm.sh/typedfetch'
        
        const cities = new Map() // Store city data
        const pollers = new Map() // Store polling intervals
        
        // Search for cities with debouncing
        let searchTimeout
        window.searchCities = async function(query) {
            clearTimeout(searchTimeout)
            const suggestions = document.getElementById('suggestions')
            
            if (query.length < 2) {
                suggestions.innerHTML = ''
                return
            }
            
            suggestions.innerHTML = '<div class="loading">Searching...</div>'
            
            // Debounce to avoid too many requests
            searchTimeout = setTimeout(async () => {
                try {
                    const { data } = await tf.get('https://api.teleport.org/api/cities/', {
                        params: { search: query }
                    })
                    
                    suggestions.innerHTML = data._embedded['city:search-results']
                        .slice(0, 5)
                        .map(city => `
                            <div class="suggestion" onclick="addCity('${city.matching_full_name}')">
                                ${city.matching_full_name}
                            </div>
                        `).join('')
                        
                } catch (error) {
                    suggestions.innerHTML = '<div class="error">Search failed</div>'
                }
            }, 300)
        }
        
        // Add a city to dashboard
        window.addCity = async function(cityName) {
            document.getElementById('citySearch').value = ''
            document.getElementById('suggestions').innerHTML = ''
            
            if (cities.has(cityName)) return // Already added
            
            const cityDiv = document.createElement('div')
            cityDiv.className = 'city-card'
            cityDiv.id = `city-${cityName.replace(/\s/g, '-')}`
            cityDiv.innerHTML = '<div class="loading">Loading weather...</div>'
            document.getElementById('cityGrid').appendChild(cityDiv)
            
            // Start polling for this city
            const stopPolling = pollWeatherForCity(cityName, cityDiv)
            pollers.set(cityName, stopPolling)
        }
        
        // Poll weather for a specific city
        function pollWeatherForCity(cityName, element) {
            let consecutiveErrors = 0
            
            async function update() {
                try {
                    const { data } = await tf.get(`https://wttr.in/${cityName}?format=j1`)
                    
                    consecutiveErrors = 0 // Reset error count
                    cities.set(cityName, data)
                    
                    element.innerHTML = `
                        <h3>${cityName}</h3>
                        <button onclick="removeCity('${cityName}')" style="float: right">×</button>
                        <p>🌡️ ${data.current_condition[0].temp_C}°C / ${data.current_condition[0].temp_F}°F</p>
                        <p>🌤️ ${data.current_condition[0].weatherDesc[0].value}</p>
                        <p>💨 Wind: ${data.current_condition[0].windspeedKmph} km/h</p>
                        <p>💧 Humidity: ${data.current_condition[0].humidity}%</p>
                        <p>🔄 Updated: ${new Date().toLocaleTimeString()}</p>
                    `
                } catch (error) {
                    consecutiveErrors++
                    
                    if (consecutiveErrors > 3) {
                        element.innerHTML = `
                            <h3>${cityName}</h3>
                            <button onclick="removeCity('${cityName}')" style="float: right">×</button>
                            <div class="error">
                                <p>Failed to load weather</p>
                                <p>${error.message}</p>
                                <button onclick="retryCity('${cityName}')">Retry</button>
                            </div>
                        `
                    }
                }
            }
            
            // Initial update
            update()
            
            // Poll every 60 seconds
            const interval = setInterval(update, 60000)
            
            return () => clearInterval(interval)
        }
        
        // Remove city from dashboard
        window.removeCity = function(cityName) {
            cities.delete(cityName)
            const poller = pollers.get(cityName)
            if (poller) {
                poller() // Stop polling
                pollers.delete(cityName)
            }
            document.getElementById(`city-${cityName.replace(/\s/g, '-')}`).remove()
        }
        
        // Retry failed city
        window.retryCity = function(cityName) {
            const element = document.getElementById(`city-${cityName.replace(/\s/g, '-')}`)
            const stopPolling = pollWeatherForCity(cityName, element)
            pollers.set(cityName, stopPolling)
        }
        
        // Add some default cities on load
        window.addEventListener('load', () => {
            ['London', 'Tokyo', 'New York'].forEach(city => addCity(city))
        })
    </script>
</head>
<body>
    <h1>Weather Buddy 3.0 - Multi-City Dashboard 🌍</h1>
    
    <div class="search-box">
        <input 
            type="text" 
            id="citySearch" 
            placeholder="Search for a city..."
            onkeyup="searchCities(this.value)"
            style="width: 300px; padding: 10px;"
        />
        <div id="suggestions" class="search-suggestions"></div>
    </div>
    
    <div id="cityGrid" class="city-grid"></div>
</body>
</html>

Advanced GET Patterns

1. Request Signing

Some APIs require signed requests:

// Example: AWS-style request signing
import { createHash, createHmac } from 'crypto'

function signRequest(secretKey, stringToSign) {
  return createHmac('sha256', secretKey)
    .update(stringToSign)
    .digest('hex')
}

const timestamp = new Date().toISOString()
const signature = signRequest(SECRET_KEY, `GET\n/api/data\n${timestamp}`)

const { data } = await tf.get('https://api.example.com/data', {
  headers: {
    'X-Timestamp': timestamp,
    'X-Signature': signature
  }
})

2. GraphQL Queries via GET

Yes, you can do GraphQL with GET:

const query = `
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      email
      posts {
        title
      }
    }
  }
`

const { data } = await tf.get('https://api.example.com/graphql', {
  params: {
    query,
    variables: JSON.stringify({ id: '123' })
  }
})

3. Response Transformation

Transform data as it arrives:

const api = createTypedFetch()

// Add response transformer
api.addResponseInterceptor(response => {
  // Convert snake_case to camelCase
  if (response.data) {
    response.data = snakeToCamel(response.data)
  }
  return response
})

// Now all responses are automatically transformed
const { data } = await api.get('/api/user_profile')
console.log(data.firstName) // was first_name

Debugging GET Requests

When things go wrong, TypedFetch helps you figure out why:

// Enable debug mode
tf.enableDebug()

// Make request
await tf.get('https://api.example.com/data')

// Console shows:
// [TypedFetch] 🚀 GET https://api.example.com/data
// [TypedFetch] 📋 Headers: { "Content-Type": "application/json" }
// [TypedFetch] ⏱️  Response time: 234ms
// [TypedFetch] ✅ Status: 200 OK
// [TypedFetch] 💾 Cached for 5 minutes

Practice Time! 🏋️

Exercise 1: GitHub Repository Explorer

Build a tool that searches GitHub repositories:

async function searchRepos(query, language = null, sort = 'stars') {
  // Your code here
  // Use: https://api.github.com/search/repositories
  // Params: q (query), language, sort, order
}

Exercise 2: Paginated Data Fetcher

Create a generic paginated fetcher:

async function* fetchAllPages(baseUrl, params = {}) {
  // Your code here
  // Should yield items one at a time
  // Should handle any paginated API
}

Exercise 3: Smart Cache Manager

Build a cache that respects cache headers:

class SmartCache {
  async get(url, options) {
    // Check cache-control headers
    // Respect max-age
    // Handle etags
  }
}

Key Takeaways 🎯

  1. GET requests are for reading data - No side effects
  2. Query parameters are your friends - Use params option
  3. Headers control behavior - Auth, versions, formats
  4. Pagination is everywhere - Plan for it
  5. Parallel requests are faster - Use Promise.all()
  6. Caching is automatic - But you can control it
  7. Debug mode shows everything - Use it when stuck

Common Pitfalls to Avoid 🚨

  1. Building URLs manually - Use params option instead
  2. Forgetting to encode values - TypedFetch does it for you
  3. Sequential requests - Parallelize when possible
  4. Ignoring pagination - Always check for more pages
  5. Over-fetching data - Request only needed fields
  6. Not handling errors - Network requests fail

What's Next?

You've mastered reading data with GET requests. But what about creating, updating, and deleting? In Chapter 4, we'll explore the full CRUD (Create, Read, Update, Delete) operations with POST, PUT, and DELETE.

We'll also evolve Weather Buddy to let users save favorite cities, customize their dashboard, and share their weather setup with friends.

Ready to start changing data instead of just reading it? See you in Chapter 4! 🚀


Chapter Summary

  • GET requests are for reading data without side effects
  • Query parameters are handled automatically with the params option
  • Headers control authentication, API versions, and response formats
  • Pagination requires looping or generators for large datasets
  • Parallel requests with Promise.all() improve performance
  • TypedFetch automatically caches GET requests
  • Polling enables real-time updates with simple setInterval
  • Debug mode reveals everything about your requests
  • Weather Buddy now supports multiple cities with live updates and search

Next Chapter Preview: POST, PUT, and DELETE - creating, updating, and deleting data. Learn to build full CRUD applications with TypedFetch.