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
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
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
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
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.