http clients and apis

API Client Generation Orientation

API client generation turns an API contract, often OpenAPI, into code that knows how to call the API. Generated clients can provide methods, request objects, response models, and typed parameters.

Generated clients save repetitive work, but they are only as good as the contract and the generation process. You still need to understand authentication, errors, pagination, retries, and upgrades.

What generated clients do

Instead of writing raw requests everywhere, generated clients give you named methods.

PHP example
<?php

declare(strict_types=1);

final class ProductsClient
{
    public function show(int $id): string
    {
        return '/products/' . $id;
    }
}

$client = new ProductsClient();

echo $client->show(123) . PHP_EOL;

// Prints:
// /products/123

Real generated clients would send the HTTP request, decode the response, and map data into response objects.

Generated code is not magic

A generated method still depends on:

  • a correct OpenAPI document
  • correct base URL configuration
  • authentication configuration
  • HTTP client behaviour
  • error handling conventions
  • regeneration when the API contract changes
PHP example
<?php

declare(strict_types=1);

final class ProductsClient
{
    public function __construct(private string $baseUrl)
    {
    }

    public function showUrl(int $id): string
    {
        return rtrim($this->baseUrl, '/') . '/products/' . $id;
    }
}

$client = new ProductsClient('https://api.example.test');

echo $client->showUrl(123) . PHP_EOL;

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

Keep custom code out of generated files

Generated files may be overwritten. Do not hand-edit generated classes unless the project explicitly treats them as owned code.

A common pattern is:

  • generated client in one namespace or directory
  • hand-written wrapper service around it
  • application code depends on the wrapper, not generated internals
PHP example
<?php

declare(strict_types=1);

final class ProductLookup
{
    public function __construct(private ProductsClient $client)
    {
    }

    public function productUrlForLog(int $id): string
    {
        return $this->client->showUrl($id);
    }
}

echo (new ProductLookup($client))->productUrlForLog(123) . PHP_EOL;

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

The wrapper is where your application-specific error translation, logging, retries, and return types often belong.

Regeneration risk

Generated clients can change a lot when the OpenAPI contract changes. A small schema change may rename models, change nullable fields, or alter method signatures.

Treat regeneration as a code change that needs review. Check generated diffs for breaking changes.

When generated clients help

Generated clients are useful when:

  • the API contract is large
  • many endpoints have typed request and response models
  • several teams or languages consume the same API
  • the OpenAPI document is well maintained
  • clients need to stay close to the published contract

They are less useful when the API is tiny, the contract is inaccurate, or the generated output is harder to use than a small hand-written client.

What to check in a project

Check where generated code lives and whether it should be edited by hand.

Check the command or process that regenerates the client.

Check whether generated code is committed or built during CI.

Check how authentication, retries, timeouts, and logging are added.

Check whether application code depends directly on generated classes or on a wrapper.

Check generated diffs carefully when updating the API spec.

What you should be able to do

After this lesson, you should be able to explain what API client generation provides, why OpenAPI quality matters, where to put custom code, and how to review generated-client changes safely.

Practice

Task: Wrap A Generated Client

Write a small PHP script that models a generated client and a hand-written wrapper around it.

Requirements

  • Use declare(strict_types=1);.
  • Create a generated-style client class with a method for /products/{id}.
  • Create a wrapper class that depends on the client.
  • Keep application-specific naming in the wrapper.
  • Include one normal case.
  • Include one edge case for an invalid ID.
  • Print the results.

Check Your Work

Run the script and confirm the wrapper is where the application rule is enforced.

Show solution

This solution treats GeneratedProductsClient as code that could be overwritten, so the application rule lives in ProductService.

PHP example
<?php

declare(strict_types=1);

final class GeneratedProductsClient
{
    public function showUrl(int $id): string
    {
        return '/products/' . $id;
    }
}

final class ProductService
{
    public function __construct(private GeneratedProductsClient $client)
    {
    }

    public function productDetailsPath(int $id): string
    {
        if ($id < 1) {
            throw new InvalidArgumentException('Product ID must be positive.');
        }

        return $this->client->showUrl($id);
    }
}

$service = new ProductService(new GeneratedProductsClient());

echo $service->productDetailsPath(123) . PHP_EOL;

try {
    $service->productDetailsPath(0);
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// /products/123
// Product ID must be positive.

Why This Works

The generated-style client only knows the endpoint shape. The wrapper owns the application-facing method name and local validation rule.