Mastering Laravel HTTP Client for External APIs

· 5 min read

Laravel's HTTP client (built on Guzzle) makes consuming external APIs simple and elegant. But building robust integrations requires understanding retry strategies, error handling, and request/response transformation.

The Basics

use Illuminate\Support\Facades\Http;

$response = Http::get('https://api.example.com/users');

if ($response->successful()) {
    $users = $response->json();
} else {
    $response->throw(); // Throws HttpRequestException
}

Adding Retry Logic

External APIs can fail temporarily. Always implement retry strategies:

$response = Http::retry(3, 100) // 3 attempts, 100ms between retries
    ->get('https://api.example.com/users');

// Exponential backoff (100ms, 200ms, 400ms)
$response = Http::retry(3, 100, exponential: true)
    ->get('https://api.example.com/users');

Timeout Handling

$response = Http::timeout(10)      // 10 second timeout
    ->connectTimeout(5)             // 5 second connection timeout
    ->get('https://api.example.com/users');

// Catch timeout exceptions
try {
    $response = Http::timeout(5)->get('https://api.example.com/users');
} catch (ConnectException | RequestException $e) {
    Log::error('API timeout: ' . $e->getMessage());
}

Authentication Patterns

Bearer Token

$response = Http::withToken($token)
    ->get('https://api.example.com/users');

Basic Auth

$response = Http::withBasicAuth('username', 'password')
    ->get('https://api.example.com/users');

Custom Headers

$response = Http::withHeaders([
    'X-API-Key' => config('services.external-api.key'),
    'Accept' => 'application/json',
])
->get('https://api.example.com/users');

Building a Reusable API Client

Instead of scattering HTTP calls throughout your app, create a dedicated service:

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\PendingRequest;

class ExternalApiClient
{
    private string $baseUrl = 'https://api.example.com';
    private string $apiKey;

    public function __construct()
    {
        $this->apiKey = config('services.external-api.key');
    }

    protected function client(): PendingRequest
    {
        return Http::retry(3, 100, exponential: true)
            ->timeout(10)
            ->connectTimeout(5)
            ->withToken($this->apiKey)
            ->baseUrl($this->baseUrl);
    }

    public function getUsers(): array
    {
        return $this->client()
            ->get('/users')
            ->throw()
            ->json();
    }

    public function createUser(array $data): array
    {
        return $this->client()
            ->post('/users', $data)
            ->throw()
            ->json();
    }

    public function updateUser(int $id, array $data): array
    {
        return $this->client()
            ->patch("/users/{$id}", $data)
            ->throw()
            ->json();
    }
}

Error Handling with Callbacks

$response = Http::get('https://api.example.com/users');

// Validate response
if ($response->status() === 429) {
    // Rate limited
    Log::warning('API rate limited. Retry-After: ' . $response->header('Retry-After'));
} elseif ($response->serverError()) {
    // 5xx error
    Log::error('API server error: ' . $response->status());
} elseif ($response->clientError()) {
    // 4xx error
    Log::warning('API client error: ' . $response->json());
}

Async Requests with Promises

For non-blocking requests:

$promises = [
    'users' => Http::async()->get('https://api.example.com/users'),
    'posts' => Http::async()->get('https://api.example.com/posts'),
];

$results = Http::pool(function ($pool) {
    $pool->get('https://api.example.com/users');
    $pool->get('https://api.example.com/posts');
});

foreach ($results as $response) {
    if ($response->successful()) {
        echo $response->json();
    }
}

Request/Response Logging

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

$response = Http::withoutRedirecting()
    ->timeout(30)
    ->get('https://api.example.com/users');

// Log everything
Log::info('API Request', [
    'method' => 'GET',
    'url' => 'https://api.example.com/users',
    'status' => $response->status(),
    'body' => $response->json(),
]);

Rate Limiting

If the external API enforces rate limits:

use Illuminate\Support\Facades\Cache;

public function getFromRateLimitedApi($url)
{
    $key = 'api_call_' . md5($url);
    
    if (Cache::has($key)) {
        throw new Exception('Rate limit exceeded. Try again later.');
    }

    $response = Http::get($url);
    
    if ($response->header('X-RateLimit-Remaining') < 10) {
        Cache::put($key, true, now()->addMinutes(5));
    }

    return $response->json();
}

Data Transformation

class ExternalApiClient
{
    public function getFormattedUsers(): array
    {
        $data = $this->client()
            ->get('/users')
            ->throw()
            ->json();

        return collect($data['users'])
            ->map(fn($user) => [
                'id' => $user['id'],
                'name' => $user['full_name'],
                'email' => $user['email_address'],
            ])
            ->toArray();
    }
}

Testing API Calls

use Illuminate\Support\Facades\Http;

public function test_can_fetch_users()
{
    Http::fake([
        'api.example.com/*' => Http::response([
            'users' => [
                ['id' => 1, 'name' => 'John'],
            ]
        ], 200),
    ]);

    $client = new ExternalApiClient();
    $users = $client->getUsers();

    $this->assertCount(1, $users);
    Http::assertSent(function ($request) {
        return $request->url() === 'https://api.example.com/users';
    });
}

Best Practices

  1. Always verify SSL certificates - Don't disable in production
  2. Set reasonable timeouts - Prevent hanging requests
  3. Implement exponential backoff - For transient failures
  4. Log everything - Debug issues quickly
  5. Use dedicated services - Encapsulate API logic
  6. Handle rate limits - Respect API quotas
  7. Transform data early - Decouple internal formats from external schemas
  8. Test with fakes - Mock external APIs in tests

Summary

Laravel's HTTP client is powerful and flexible. By combining retry logic, timeout handling, proper authentication, and error management, you can build robust integrations with any external API.

Key takeaways:

  • Use retry with exponential backoff
  • Always set timeouts
  • Create dedicated service classes
  • Mock APIs in tests
  • Log requests and responses
  • Transform data at API boundaries

Comments