http clients and apis

Guzzle Orientation

Guzzle is a widely used PHP HTTP client. It wraps lower-level transport details behind a consistent API for sending requests, reading responses, handling errors, adding middleware, and testing integrations.

This lesson is an orientation. You should understand the shape of Guzzle code even if the package is not installed in this training project.

Why teams use Guzzle

Compared with raw streams or direct cURL, Guzzle gives you:

  • clear request option arrays
  • response objects with status, headers, and body
  • consistent exceptions
  • middleware for logging, retries, and signing
  • test helpers such as mock handlers
  • PSR-7 request and response support

Request option shape

A Guzzle request usually starts with a method, URL or path, and an options array.

PHP example
<?php

declare(strict_types=1);

$request = [
    'method' => 'GET',
    'uri' => '/products',
    'options' => [
        'base_uri' => 'https://api.example.test',
        'headers' => ['Accept' => 'application/json'],
        'timeout' => 5,
    ],
];

echo $request['method'] . ' ' . $request['options']['base_uri'] . $request['uri'] . PHP_EOL;

// Prints:
// GET https://api.example.test/products

In real Guzzle code, a client may be created with base_uri, then each request uses a relative URI.

JSON requests

Guzzle has a json option. It JSON-encodes the payload and sets the appropriate content type.

PHP example
<?php

declare(strict_types=1);

$options = [
    'json' => [
        'name' => 'Keyboard',
        'price' => 99.99,
    ],
    'headers' => [
        'Accept' => 'application/json',
    ],
    'timeout' => 5,
];

echo json_encode($options['json'], JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"name":"Keyboard","price":99.99}

Do not pass both json and a manually encoded body for the same request unless you have a very specific reason. Choose one request-body strategy.

Query parameters

Guzzle also has a query option for query strings.

PHP example
<?php

declare(strict_types=1);

$options = [
    'query' => [
        'search' => 'PHP & APIs',
        'page' => 2,
    ],
];

echo http_build_query($options['query']) . PHP_EOL;

// Prints:
// search=PHP+%26+APIs&page=2

This keeps encoding separate from the path and avoids hand-built URLs.

Responses and exceptions

Guzzle responses expose status code, headers, and body. By default, Guzzle throws exceptions for many 4xx and 5xx responses unless http_errors is disabled.

You need to decide which style your client uses:

  • catch exceptions and translate them into application errors
  • disable http_errors and inspect status codes yourself
PHP example
<?php

declare(strict_types=1);

function responseOutcome(int $statusCode): string
{
    return $statusCode >= 200 && $statusCode < 300
        ? 'success'
        : 'handle status ' . $statusCode;
}

echo responseOutcome(422) . PHP_EOL;

// Prints:
// handle status 422

The important point is consistency. Do not let every caller handle API failures differently.

A small API client wrapper

Teams often wrap Guzzle in a focused client class. The rest of the application calls methods such as findProduct() rather than building HTTP requests everywhere.

PHP example
<?php

declare(strict_types=1);

final class ProductApiRequest
{
    /**
     * @return array{method: string, uri: string, options: array<string, mixed>}
     */
    public function findProduct(int $id): array
    {
        return [
            'method' => 'GET',
            'uri' => '/products/' . $id,
            'options' => [
                'headers' => ['Accept' => 'application/json'],
                'timeout' => 5,
            ],
        ];
    }
}

$request = (new ProductApiRequest())->findProduct(42);

echo $request['method'] . ' ' . $request['uri'] . PHP_EOL;

// Prints:
// GET /products/42

In production, that wrapper would hold a real GuzzleHttp\ClientInterface and return parsed application data.

Middleware and testing

Guzzle middleware can add behaviour around requests, such as logging, retry decisions, request IDs, or authentication signatures.

For tests, Guzzle's mock handler pattern lets code receive prepared responses without making network calls. That is important because unit and integration tests should not depend on a third-party API being online.

What to check in a project

Check client configuration for base_uri, timeouts, and default headers.

Check whether http_errors is enabled or disabled and how failures are handled.

Check that credentials are injected from configuration and not hard-coded.

Check that repeated request logic is wrapped in a focused API client instead of duplicated across controllers.

Check that tests use mocks, fakes, or recorded fixtures rather than real external requests for normal test runs.

What you should be able to do

After this lesson, you should be able to recognise Guzzle request options, understand json and query, reason about response handling, and see why teams wrap Guzzle in small API client classes.

Practice

Task: Shape A Guzzle Request

Write a small PHP script that builds request option arrays in the style you would pass to Guzzle.

Requirements

  • Use declare(strict_types=1);.
  • Create a function for a GET /products request with query parameters.
  • Create a function for a POST /products request with a JSON payload.
  • Include Accept: application/json.
  • Include a timeout.
  • Print the method, URI, and encoded query or body for both requests.

Check Your Work

Run the script and confirm query data and JSON body data are kept in different option keys.

Show solution

This solution prepares arrays that mirror Guzzle request options without requiring Guzzle to be installed.

PHP example
<?php

declare(strict_types=1);

/**
 * @return array{method: string, uri: string, options: array<string, mixed>}
 */
function listProductsRequest(string $search, int $page): array
{
    return [
        'method' => 'GET',
        'uri' => '/products',
        'options' => [
            'query' => ['search' => $search, 'page' => $page],
            'headers' => ['Accept' => 'application/json'],
            'timeout' => 5,
        ],
    ];
}

/**
 * @return array{method: string, uri: string, options: array<string, mixed>}
 */
function createProductRequest(string $name, float $price): array
{
    return [
        'method' => 'POST',
        'uri' => '/products',
        'options' => [
            'json' => ['name' => $name, 'price' => $price],
            'headers' => ['Accept' => 'application/json'],
            'timeout' => 5,
        ],
    ];
}

$list = listProductsRequest('PHP & APIs', 2);
$create = createProductRequest('Keyboard', 99.99);

echo $list['method'] . ' ' . $list['uri'] . '?' . http_build_query($list['options']['query']) . PHP_EOL;
echo $create['method'] . ' ' . $create['uri'] . ' ' . json_encode($create['options']['json'], JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// GET /products?search=PHP+%26+APIs&page=2
// POST /products {"name":"Keyboard","price":99.99}

Why This Works

The GET request keeps query parameters in query. The POST request keeps JSON payload data in json. Both include headers and timeout options that a real API client would need.