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
17 KiB
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.
Real Example: Building a Smart City Search
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 🎯
- GET requests are for reading data - No side effects
- Query parameters are your friends - Use params option
- Headers control behavior - Auth, versions, formats
- Pagination is everywhere - Plan for it
- Parallel requests are faster - Use Promise.all()
- Caching is automatic - But you can control it
- Debug mode shows everything - Use it when stuck
Common Pitfalls to Avoid 🚨
- Building URLs manually - Use params option instead
- Forgetting to encode values - TypedFetch does it for you
- Sequential requests - Parallelize when possible
- Ignoring pagination - Always check for more pages
- Over-fetching data - Request only needed fields
- 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.