Practical guide
How to build an API client in PHP
Keep HTTP details inside one small class. The rest of the application can then work with your data instead of a third-party API.
First, the short version
What should an API client do?
An API client is the part of an application that builds an HTTP request, sends it to a remote server, and translates the response into a shape your code understands.
A good boundary between client and server keeps URLs, headers, authentication, and foreign field names in one place. Callers receive your data object or a meaningful exception.
Get ready
What you need
Start with one safe read operation. Before writing a client for the entire API, verify one real request and response.
- PHP 8.2 or newer and Composer.
- Official API documentation: the base URL, endpoint, method, authentication, error statuses, and a response example.
- A test account or token stored in an environment variable. A secret does not belong in source code or logs.
- One small operation, such as fetching a product by ID, plus a sample successful and failed response.
Steps 1 to 3
Build a client with a clear boundary
The HTTP library handles transport. Your class must handle the contract, errors, and data mapping.
1. Install an HTTP client and set shared options
- Use a maintained library instead of wrapping cURL yourself. Symfony HttpClient also works outside a Symfony project.
- Set the base URL, Accept header, authentication, and a short timeout in one place. Do not repeat URLs or tokens in every method.
- Distinguish the timeout of one network operation from the maximum duration of the whole request. A remote service must not hold a PHP process indefinitely.
- Send the token in the appropriate HTTP header specified by the provider and never write it to logs.
composer require symfony/http-client Official Symfony HTTP client documentation 2. Create one class for the remote API
- Name the client after the service, for example CatalogApiClient. Its public methods should describe operations, not HTTP details.
- Build the request inside the client. Escape URL parameters and pass query parameters through the query option instead of concatenating strings.
- Read the HTTP status code first. A 404 may mean a missing result, while 401, 429, and 5xx responses require different failures.
- Map the JSON response to your own DTO and validate required fields and data types. Do not let a foreign associative array spread through the application.
$http = HttpClient::createForBaseUri($_ENV['CATALOG_API_URL'], [
'auth_bearer' => $_ENV['CATALOG_API_TOKEN'],
'headers' => ['Accept' => 'application/json'],
'timeout' => 5,
'max_duration' => 10,
]);
$response = $http->request('GET', '/v1/products/42');
$status = $response->getStatusCode();
$data = $response->toArray(false); Official documentation for making requests 3. Design errors, retries, and logging
- Translate a network error, timeout, and invalid response into your own exceptions. Callers then do not need to know a particular library’s exceptions.
- Use a retry only for transient failures, with a limited number of attempts and increasing delays. Respect Retry-After for a 429 response.
- Do not automatically retry write requests unless the service guarantees idempotency or supports an idempotency key. Otherwise, you may create a duplicate payment or order.
- Log the operation, duration, HTTP status, and a safe correlation ID. Leave out bodies with personal data and the authorization header.
Step 4
Verify the client against the contract
A successful response test is not enough. The client must anticipate a slow, failed, and incomplete response.
-
Run a harmless smoke test
Call one read endpoint in the test environment. Check the resulting DTO, HTTP status, and that the log does not contain the token.
php scripts/catalog-api-smoke-test.php -
Replace the transport with a test implementation
Return prepared 200, 404, 429, and 500 responses in a unit test. Verify the exact request and the client’s public behaviour without a real network.
vendor/bin/phpunit tests/Integration/CatalogApiClientTest.php -
Simulate a malformed response
Return invalid JSON or omit a required field. The client should end with a controlled exception, not a missing array key warning.
If something goes wrong
Common problems
The client waits for too long
Set both a timeout and a maximum request duration, and measure elapsed time. A higher timeout usually only delays the same failure.
An error response looks like normal data
Decide explicitly from the HTTP status before mapping the body. Return a result or your own domain exception for each expected status.
A token appears in a log
Redact Authorization and other secret headers in the central logger. Do not log entire request or response objects.
A changed foreign field breaks several parts of the application
Map the response to your own DTO inside the client. You can then fix and test a renamed field in one place.
Done
Your API client has a firm boundary.
You now call the API through one testable layer. Add operations one at a time and define the request, response, and failure behaviour for each one.