Skip to Content

Thunder

Thunder is Zeus’s custom fetch client that gives you full control over the HTTP request while maintaining complete type safety.

Overview

While Chain is simple and convenient, Thunder lets you:

  • Customize the fetch implementation
  • Add request interceptors
  • Handle retries and timeouts
  • Implement custom caching
  • Control request/response transformation

Creating Thunder

import { Thunder } from './zeus'; const thunder = Thunder(async (query, variables) => { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const json = await response.json(); if (json.errors) { throw new Error(json.errors[0].message); } return json.data; }); // Use it like Chain const result = await thunder('query')({ user: [{ id: '123' }, { name: true, email: true }], });

Authentication

Bearer Token

const thunder = Thunder(async (query, variables) => { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.API_TOKEN}`, }, body: JSON.stringify({ query, variables }), }); return (await response.json()).data; });

Dynamic Authentication

let authToken = ''; export function setAuthToken(token: string) { authToken = token; } const thunder = Thunder(async (query, variables) => { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authToken ? `Bearer ${authToken}` : '', }, body: JSON.stringify({ query, variables }), }); return (await response.json()).data; });

Token Refresh

const thunder = Thunder(async (query, variables) => { const makeRequest = async (token: string) => { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ query, variables }), }); return response; }; let response = await makeRequest(currentToken); // Refresh token on 401 if (response.status === 401) { const newToken = await refreshAuthToken(); response = await makeRequest(newToken); } const json = await response.json(); return json.data; });

Retry Logic

Exponential Backoff

const thunder = Thunder(async (query, variables) => { const maxRetries = 3; let lastError: Error | null = null; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); if (response.ok) { return (await response.json()).data; } // Don't retry client errors if (response.status >= 400 && response.status < 500) { throw new Error(`Client error: ${response.status}`); } lastError = new Error(`Server error: ${response.status}`); } catch (error) { lastError = error as Error; } // Exponential backoff: 1s, 2s, 4s if (attempt < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000)); } } throw lastError || new Error('Request failed after retries'); });

Retry with Jitter

const thunder = Thunder(async (query, variables) => { const maxRetries = 3; const baseDelay = 1000; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); if (response.ok) { return (await response.json()).data; } if (response.status < 500) { throw new Error(`HTTP ${response.status}`); } } catch (error) { if (attempt === maxRetries - 1) throw error; // Exponential backoff with jitter const delay = baseDelay * Math.pow(2, attempt); const jitter = Math.random() * delay * 0.1; await new Promise((resolve) => setTimeout(resolve, delay + jitter)); } } throw new Error('Max retries exceeded'); });

Timeout Handling

const thunder = Thunder(async (query, variables) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout try { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), signal: controller.signal, }); clearTimeout(timeout); return (await response.json()).data; } catch (error) { clearTimeout(timeout); if ((error as Error).name === 'AbortError') { throw new Error('Request timeout after 10 seconds'); } throw error; } });

Request Logging

const thunder = Thunder(async (query, variables) => { const startTime = Date.now(); console.log('GraphQL Request:', { query: query.substring(0, 100), variables, timestamp: new Date().toISOString(), }); try { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); const json = await response.json(); const duration = Date.now() - startTime; console.log('GraphQL Response:', { status: response.status, duration: `${duration}ms`, hasErrors: !!json.errors, }); return json.data; } catch (error) { const duration = Date.now() - startTime; console.error('GraphQL Error:', { duration: `${duration}ms`, error: (error as Error).message, }); throw error; } });

Error Handling

Detailed Error Handling

class GraphQLError extends Error { constructor(message: string, public statusCode?: number, public graphQLErrors?: any[]) { super(message); this.name = 'GraphQLError'; } } const thunder = Thunder(async (query, variables) => { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); const json = await response.json(); if (json.errors) { throw new GraphQLError('GraphQL errors occurred', response.status, json.errors); } if (!response.ok) { throw new GraphQLError(`HTTP ${response.status}`, response.status); } return json.data; }); // Usage try { const result = await thunder('query')({ user: [{ id: '123' }, { name: true }], }); } catch (error) { if (error instanceof GraphQLError) { console.error('Status:', error.statusCode); console.error('GraphQL Errors:', error.graphQLErrors); } }

Custom Headers Per Request

type ThunderContext = { headers?: Record<string, string>; }; const thunder = Thunder(async (query, variables, context?: ThunderContext) => { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', ...context?.headers, }, body: JSON.stringify({ query, variables }), }); return (await response.json()).data; }); // Usage with custom headers const result = await thunder('query', { headers: { 'X-Request-ID': 'abc123' }, })({ user: [{ id: '123' }, { name: true }], });

Request Caching

const cache = new Map<string, { data: any; timestamp: number }>(); const CACHE_TTL = 60000; // 1 minute const thunder = Thunder(async (query, variables) => { const cacheKey = JSON.stringify({ query, variables }); const cached = cache.get(cacheKey); // Return cached data if valid if (cached && Date.now() - cached.timestamp < CACHE_TTL) { console.log('Returning cached response'); return cached.data; } // Fetch new data const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); const data = (await response.json()).data; // Cache the result cache.set(cacheKey, { data, timestamp: Date.now() }); return data; });

File Upload

const thunder = Thunder(async (query, variables) => { // Check if variables contain File objects const hasFiles = Object.values(variables || {}).some((value) => value instanceof File || value instanceof Blob); if (hasFiles) { // Use multipart form data for file uploads const formData = new FormData(); formData.append('operations', JSON.stringify({ query, variables: {} })); const map: Record<string, string[]> = {}; let fileIndex = 0; Object.entries(variables || {}).forEach(([key, value]) => { if (value instanceof File || value instanceof Blob) { map[fileIndex] = [`variables.${key}`]; formData.append(fileIndex.toString(), value); fileIndex++; } }); formData.append('map', JSON.stringify(map)); const response = await fetch('https://api.com/graphql', { method: 'POST', body: formData, }); return (await response.json()).data; } // Regular JSON request const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); return (await response.json()).data; });

Using with Node.js

import fetch from 'node-fetch'; import { Thunder } from './zeus'; const thunder = Thunder(async (query, variables) => { const response = await fetch('https://api.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); return (await response.json()).data; });

Benefits Over Chain

Full Control

Thunder gives you complete control over the request lifecycle:

const thunder = Thunder(async (query, variables) => { // Before request await beforeRequestHook(query, variables); // Custom fetch logic const data = await customFetch(query, variables); // After request await afterRequestHook(data); return data; });

Integration with Existing Code

Thunder integrates easily with existing fetch configurations:

// Use your existing fetch setup import { authenticatedFetch } from './lib/fetch'; const thunder = Thunder(async (query, variables) => { const response = await authenticatedFetch('https://api.com/graphql', { method: 'POST', body: JSON.stringify({ query, variables }), }); return (await response.json()).data; });

Testing

Mock Thunder for testing:

// In tests const mockThunder = Thunder(async (query, variables) => { return mockData; // Return mock data based on query }); // Production const thunder = Thunder(async (query, variables) => { // Real implementation });

Chain vs Thunder

FeatureChainThunder
Simplicity✅ Very simple⚠️ More complex
Customization❌ Limited✅ Full control
Fetch Control❌ No✅ Yes
Built-in Retries❌ No✅ Custom
Request Logging❌ No✅ Custom
File Uploads❌ No✅ Custom
Best ForQuick prototypesProduction apps

Next Steps

Explore Generated Types →

Last updated on