Module 9: API Client Integration
Module Overview
Your Terraform provider is only as reliable as the API client it uses. This module teaches you to build production-grade API clients that handle authentication, retries, rate limiting, and errors gracefully.
What You’ll Learn:
- API client architecture patterns
- HTTP client configuration and best practices
- Authentication methods (API keys, OAuth, tokens)
- Comprehensive error handling
- Rate limiting and exponential backoff
- Testing API clients
- Real-world integration patterns
Time: 4-5 hours
Why API Client Design Matters
The API client is the bridge between your provider and the cloud service:
┌─────────────────────────────────────────────────────┐
│ Provider Architecture │
├─────────────────────────────────────────────────────┤
│ │
│ User Config → Provider → API Client → Cloud API │
│ ↑ ↑ │
│ │ │ │
│ Schema/Resources HTTP/Auth/Retry │
│ State Management Error Handling │
│ │
└─────────────────────────────────────────────────────┘
A good API client:
- Handles authentication automatically
- Retries transient failures
- Respects rate limits
- Provides clear error messages
- Logs requests for debugging
- Supports testing with mocks
A poor API client:
- Fails on transient errors
- Exposes raw HTTP errors
- Doesn’t handle rate limits
- Hard to test
- No logging or debugging support
API Client Responsibilities
1. Authentication
Handle all authentication methods:
type Client struct {
httpClient *http.Client
apiKey string
baseURL string
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*Response, error) {
// Add authentication
req.Header.Set("Authorization", "Bearer "+c.apiKey)
// Make request
return c.do(req)
}
2. Request Construction
Build correct requests:
func (c *Client) CreateDatabase(ctx context.Context, input CreateDatabaseInput) (*Database, error) {
// Construct request
body := map[string]interface{}{
"name": input.Name,
"engine": input.Engine,
"storage_gb": input.StorageGB,
}
// POST /api/v2/databases
resp, err := c.post(ctx, "/databases", body)
// Parse response
var db Database
json.Unmarshal(resp.Body, &db)
return &db, nil
}
3. Error Handling
Translate API errors to meaningful messages:
func (c *Client) do(req *http.Request) (*Response, error) {
resp, err := c.httpClient.Do(req)
if resp.StatusCode == 404 {
return nil, ErrNotFound
}
if resp.StatusCode == 429 {
return nil, ErrRateLimited
}
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("API error: %s", resp.Status)
}
return resp, nil
}
4. Retry Logic
Retry transient failures:
func (c *Client) do(req *http.Request) (*Response, error) {
var resp *http.Response
var err error
for attempt := 0; attempt < maxRetries; attempt++ {
resp, err = c.httpClient.Do(req)
if err == nil && resp.StatusCode < 500 {
break
}
// Exponential backoff
time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second)
}
return resp, err
}
5. Rate Limiting
Respect API rate limits:
type Client struct {
httpClient *http.Client
rateLimiter *rate.Limiter
}
func (c *Client) do(req *http.Request) (*Response, error) {
// Wait for rate limit
err := c.rateLimiter.Wait(req.Context())
if err != nil {
return nil, err
}
// Make request
return c.httpClient.Do(req)
}
Common API Client Patterns
Pattern 1: Simple REST Client
type Client struct {
httpClient *http.Client
apiKey string
baseURL string
}
func (c *Client) GetDatabase(ctx context.Context, id string) (*Database, error) {
url := fmt.Sprintf("%s/databases/%s", c.baseURL, id)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var db Database
json.NewDecoder(resp.Body).Decode(&db)
return &db, nil
}
Pattern 2: SDK-Based Client
import "github.com/cloud-provider/sdk-go"
type Client struct {
sdk *sdk.Client
}
func NewClient(apiKey string) (*Client, error) {
sdkClient, err := sdk.NewClient(&sdk.Config{
APIKey: apiKey,
Region: "us-east-1",
})
return &Client{sdk: sdkClient}, nil
}
func (c *Client) GetDatabase(ctx context.Context, id string) (*Database, error) {
// Use SDK
output, err := c.sdk.GetDatabase(&sdk.GetDatabaseInput{
ID: id,
})
// Convert SDK types to provider types
db := &Database{
ID: *output.ID,
Name: *output.Name,
Engine: *output.Engine,
}
return db, nil
}
Pattern 3: GraphQL Client
type Client struct {
graphql *graphql.Client
}
func (c *Client) GetDatabase(ctx context.Context, id string) (*Database, error) {
query := `
query GetDatabase($id: ID!) {
database(id: $id) {
id
name
engine
status
}
}
`
var response struct {
Database Database `json:"database"`
}
err := c.graphql.Query(ctx, query, map[string]interface{}{
"id": id,
}, &response)
return &response.Database, err
}
Module Chapters
Chapter 1: API Client Architecture
Design patterns for API clients, interface design, and separation of concerns.
Chapter 2: HTTP Client Configuration
Configure HTTP clients with timeouts, keep-alive, TLS, and custom transports.
Chapter 3: Authentication Patterns
Implement API key, OAuth, JWT, and certificate-based authentication.
Chapter 4: Error Handling
Handle API errors, create custom error types, and provide meaningful messages.
Chapter 5: Rate Limiting & Retries
Implement rate limiting, exponential backoff, and intelligent retry logic.
Chapter 6: API Client Lab
Hands-on: Build a complete API client with authentication, retries, and error handling.
Chapter 7: API Client Practice
Test your knowledge with comprehensive practice scenarios.
Learning Path
Architecture → HTTP Config → Auth → Errors → Retries → Lab
↓ ↓ ↓ ↓ ↓ ↓
Design Timeouts API Keys Custom Backoff Practice
Patterns TLS OAuth Types Limits
What You’ll Build
By the end of this module, you’ll have a production-ready API client:
Client Interface:
type Client interface {
GetDatabase(ctx context.Context, id string) (*Database, error)
CreateDatabase(ctx context.Context, input CreateDatabaseInput) (*Database, error)
UpdateDatabase(ctx context.Context, id string, input UpdateDatabaseInput) (*Database, error)
DeleteDatabase(ctx context.Context, id string) error
ListDatabases(ctx context.Context) ([]Database, error)
}
Implementation with:
- ✅ Automatic authentication
- ✅ Exponential backoff retries
- ✅ Rate limiting
- ✅ Custom error types
- ✅ Request/response logging
- ✅ Context cancellation support
- ✅ Testable with mocks
- ✅ TLS configuration
Key Concepts
HTTP Client: Configurable client for making HTTP requests
Authentication: Proving identity to API (API key, OAuth, JWT)
Rate Limiting: Controlling request frequency
Exponential Backoff: Increasing wait time between retries
Circuit Breaker: Preventing cascading failures
Idempotency: Safe to retry operations
Context: Cancellation and timeout management
Best Practices Preview
Throughout this module, you’ll learn:
- ✅ Use context for cancellation and timeouts
- ✅ Implement exponential backoff for retries
- ✅ Respect API rate limits
- ✅ Create custom error types
- ✅ Log requests for debugging
- ✅ Make clients testable with interfaces
- ✅ Configure reasonable timeouts
- ✅ Handle authentication transparently
- ✅ Support user-agent headers
- ✅ Use connection pooling
Common Pitfalls to Avoid
- ❌ No retry logic for transient failures
- ❌ Exposing raw HTTP errors to users
- ❌ Not handling rate limits
- ❌ Infinite retries without backoff
- ❌ No timeout configuration
- ❌ Ignoring context cancellation
- ❌ Hard-coding credentials
- ❌ Not logging for debugging
- ❌ Untestable API clients
- ❌ Not reusing HTTP connections
Real-World Examples
AWS SDK Pattern:
import "github.com/aws/aws-sdk-go-v2/service/rds"
type Client struct {
rds *rds.Client
}
func (c *Client) GetDatabase(ctx context.Context, id string) (*Database, error) {
output, err := c.rds.DescribeDBInstances(ctx, &rds.DescribeDBInstancesInput{
DBInstanceIdentifier: &id,
})
// Convert AWS types to provider types
return convertAWSDatabase(output.DBInstances[0]), nil
}
Custom REST API:
type Client struct {
httpClient *http.Client
baseURL string
apiKey string
retrier *retrier.Retrier
}
func (c *Client) CreateDatabase(ctx context.Context, input CreateDatabaseInput) (*Database, error) {
var db Database
err := c.retrier.Do(ctx, func() error {
resp, err := c.post(ctx, "/databases", input)
if err != nil {
return err
}
return json.Unmarshal(resp, &db)
})
return &db, err
}
Prerequisites
Before starting this module, you should have completed:
- ✅ Module 6: Resource Implementation
- ✅ Module 8: State Management
Go knowledge needed:
- HTTP client usage
- Context handling
- Error handling
- Interfaces and testing
Get Started
Ready to build a robust API client?
Start with API Client Architecture →
Previous: ← State Management Practice
Last updated: July 2, 2026